4
votes

Ruby: Comment utiliser dup / clone pour ne pas muter une variable d'instance d'origine?

En apprenant Ruby, je crée un projet Battleship et j'ai le code suivant comme méthode d'instance pour une classe "Board" que je crée.

  1) Board PART 2 #hidden_ships_grid should not mutate the original @grid
     Failure/Error: expect(board.instance_variable_get(:@grid)).to eq([[:S, :N],[:X, :S]])

       expected: [[:S, :N], [:X, :S]]
            got: [[:N, :N], [:X, :N]]

       (compared using ==)

       Diff:
       @@ -1,2 +1,2 @@
       -[[:S, :N], [:X, :S]]
       +[[:N, :N], [:X, :N]]

Fondamentalement, cette méthode créerait une autre instance d'une variable @grid qui remplacerait à la place chaque symbole: S par a: N.

Le RSPEC a deux exigences: 1) "doit retourner un tableau 2D représentant la grille où chaque: S est remplacé par un: N" et 2) "ne doit pas muter la @grid d'origine". p>

Mon problème est que mon code ci-dessus satisfait la première exigence, mais il brise la deuxième exigence. Quelqu'un peut-il m'expliquer ce qui cause la mutation du fichier @grid d'origine? J'ai parcouru le code 15 fois et je ne peux pas voir où je réécris ou réaffecte la variable @grid d'origine.

La solution "correcte" qui nous est fournie utilise ".map", ce qui est bien, mais je veux comprendre pourquoi cette solution ne fonctionne pas et finit par muter la variable @grid d'origine.

def hidden_ships_grid
    hidden_s_grid = @grid.dup 
    hidden_s_grid.each_with_index do |sub_array, i|
        sub_array.each_with_index do |el, j|
            # position = [i, j]
            hidden_s_grid[i][j] = :N if el == :S 
       end
    end
end

Merci d'avance!


3 Réponses :


3
votes

dup et clone sont superficiels Vous copiez le contenu du tableau de grille qui fait référence au tableau interne. Ces tableaux ne sont pas copiés, mais référencés. C'est pourquoi la grille d'origine est modifiée plus tard.

Vous devez mapper sur une grille et dupliquer des tableaux internes


4 commentaires

Ou copiez le tout en une fois: hidden_s_grid = Marshal.load (Marshal.dump (@grid))


Le rassemblement n'est-il pas une solution plus lente que la cartographie?


Ou vous pouvez utiliser les gemmes full_dup ou full_clone pour faire des copies complètes de votre plateau de jeu. Divulgation complète: je suis l'auteur de ces gemmes. Comme dans: hidden_s_grid = @ grid.full_dup


@mrzasa: cela doit être mesuré. J'ai perdu le compte au nombre de fois où j'ai été surpris par les résultats de référence.



7
votes

C'est une erreur courante pour les débutants.

Supposons que

b = a.map { |arr0| arr0.dup.map { |arr1| arr1.dup } }
  #=> [[1, [2, 3]], [[4, 5], 6]] 
b[0][1][0] = 'cat'
b #=> [[1, ["cat", 3]], [[4, 5], 6]] 
a #=> [[1, [2, 3]], [[4, 5], 6]]

C'est exactement ce que vous attendiez et espériez. Considérez maintenant ce qui suit:

a = [[1, [2, 3]], [[4, 5], 6]]

Encore une fois, c'est le résultat souhaité. Encore un:

b[0][0] = 'cat'
  #=> "cat" 
b #=> [["cat", 2], [3, 4]] 
a #=> [[1, 2], [3, 4]]  

Aarrg! C'est le problème que vous avez rencontré. Pour voir ce qui se passe ici, regardons les identifiants des différents objets qui composent a et b . Rappelez-vous que chaque objet Ruby a un Object # id a >.

a = [[1, 2], [3, 4]]
b = a.dup.map(&:dup) # same as a.dup.map { |arr| arr.dup }
  #=> [[1, 2], [3, 4]] 
a.map(&:object_id)
  #=> [...180, ...120] 
b.map(&:object_id)
  #=> [...080, ...040] 

Ici, nous remplaçons simplement b [0] , qui était initialement l'objet a [0] par un objet différent ( 'cat' ) qui a bien sûr un identifiant différent. Cela n'affecte pas a . (Dans ce qui suit, je ne donnerai que les trois derniers chiffres des identifiants. Si deux sont identiques, l'identifiant complet est le même.) Maintenant, considérez ce qui suit.

a = [[1, 2], [3, 4]]
b = a.dup
a.map(&:object_id)
  #=> [...620, ...600] 
b.map(&:object_id)
  #=> [...620, ...600] 
b[0][0] = 'cat'
  #=> "cat" 
b #=> [["cat", 2], [3, 4]] 
a #=> [["cat", 2], [3, 4]] 
a.map(&:object_id)
  #=> [...620, ...600] 
b.map(&:object_id)
  #=> [...620, ...600] 

Nous voyons que les éléments de a et b sont les mêmes objets qu'ils étaient avant l'exécution de b [0] [0] = 'cat' . Cette affectation a cependant modifié la valeur de l'objet dont l'id est ... 620 , ce qui explique pourquoi a , ainsi que b , a été modifié.

Pour éviter de modifier a , nous devons faire ce qui suit.

a = [[1, 2], [3, 4]]
b = a.dup
a.map(&:object_id)
  #=> [48959475855260, 48959475855240] 
b.map(&:object_id)
  #=> [48959475855260, 48959475855240] 
b[0] = 'cat'
b #=> ["cat", [3, 4]] 
a #=> [[1, 2], [3, 4]] 
b.map(&:object_id)
  #=> [48959476667580, 48959475855240] 

Maintenant, les éléments de b sont des objets différents de ceux de a , donc toute modification apportée à b n'affectera pas a :

a = [[1,2], [3,4]]
b = a.dup
  #=> [[1,2], [3,4]]
b[0][0] = 'cat'
b #=> [["cat", 2], [3, 4]] 
a #=> [["cat", 2], [3, 4]] 

Si nous avions

a = [[1, 2], [3, 4]]
b = a.dup
  #=> [[1, 2], [3, 4]]
b[0] = 'cat'
b #=> ["cat", [3, 4]] 
a #=> [[1, 2], [3, 4]] 

, nous aurions besoin de dup à trois niveaux:

a = [1, 2, 3]
b = a.dup
  #=> [[1, 2], [3, 4]]
b[0] = 'cat'
  #=> "cat" 
b #=> ["cat", 2, 3] 
a #=> [1, 2, 3] 

et ainsi de suite.


2 commentaires

Merci, j'ai oublié que Ruby n'a pas de deepcopy , mais je me demande pourquoi. Pourquoi ne pas ajouter à 2.6? Python l'a.


@iGan, bonne question. Je suis sûr que les moines Ruby y ont pensé. On peut obtenir une copie complète pour la plupart des objets avec Marshal.load (Marshall (dump (obj))) . Voir Marshal .