112 votes

Comportement étrange et inattendu (disparition/changement des valeurs) lors de l'utilisation de la valeur par défaut d'un Hash, par exemple Hash.new([])

Considérez ce code :

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

C'est très bien, mais :

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                   Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}       Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}   Where is `3`?

A ce stade, je m'attends à ce que le hachage soit.. :

{1=>[1], 2=>[2], 3=>[3]}

mais c'est loin d'être le cas. Que se passe-t-il et comment puis-je obtenir le comportement que j'attends ?

179voto

Andrew Marshall Points 43955

Tout d'abord, notez que ce comportement s'applique à toute valeur par défaut qui est ensuite modifiée (par exemple, les hachages et les chaînes de caractères), et pas seulement aux tableaux. Il s'applique également de manière similaire aux éléments peuplés de Array.new(3) { [] } .

TL;DR : Utiliser Hash.new { |h, k| h[k] = [] } si vous voulez la solution la plus idiomatique et que vous ne vous souciez pas du pourquoi.


Ce qui ne marche pas

Pourquoi Hash.new([]) ne fonctionne pas

Voyons plus en détail pourquoi Hash.new([]) ne fonctionne pas :

h = Hash.new([])
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["a", "b"]
h[1]         #=> ["a", "b"]

h[0].object_id == h[1].object_id  #=> true
h  #=> {}

Nous pouvons voir que notre objet par défaut est réutilisé et muté (c'est parce qu'il est passé comme la seule et unique valeur par défaut, le hash n'a aucun moyen d'obtenir une nouvelle valeur par défaut), mais pourquoi il n'y a pas de clés ou de valeurs dans le tableau, alors que h[1] Vous nous donnez toujours une valeur ? Voici un indice :

h[42]  #=> ["a", "b"]

Le tableau retourné par chaque [] est juste la valeur par défaut, que nous avons mutée pendant tout ce temps et qui contient maintenant nos nouvelles valeurs. Depuis << n'assigne pas au hash (il ne peut jamais y avoir d'assignation en Ruby sans une fonction = présent † ), nous n'avons jamais rien mis dans notre hachage réel. A la place, nous devons utiliser <<= (ce qui revient à << comme += est de + ) :

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}

C'est la même chose que :

h[2] = (h[2] << 'c')

Pourquoi Hash.new { [] } ne fonctionne pas

Utilisation de Hash.new { [] } résout le problème de la réutilisation et de la mutation de la valeur par défaut originale (puisque le bloc donné est appelé à chaque fois, renvoyant un nouveau tableau), mais pas le problème d'affectation :

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

Ce qui fonctionne

La méthode d'affectation

Si nous nous rappelons de toujours utiliser <<= entonces Hash.new { [] } es une solution viable, mais c'est un peu bizarre et non-idiomatique (je n'ai jamais vu <<= utilisés dans la nature). Il est également sujet à de subtils bogues si << est utilisé par inadvertance.

La voie mutable

El la documentation pour Hash.new (c'est moi qui souligne) :

Si un bloc est spécifié, il sera appelé avec l'objet de hachage et la clé, et devrait retourner la valeur par défaut. Il incombe au bloc de stocker la valeur dans le hachage si nécessaire. .

Nous devons donc stocker la valeur par défaut dans le hachage à l'intérieur du bloc si nous souhaitons utiliser la fonction << au lieu de <<= :

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}

Ceci déplace effectivement l'affectation de nos appels individuels (qui utiliseraient <<= ) au bloc transmis à Hash.new en supprimant le risque de comportement inattendu lors de l'utilisation de << .

Notez qu'il y a une différence fonctionnelle entre cette méthode et les autres : cette méthode affecte la valeur par défaut à la lecture (car l'affectation se fait toujours à l'intérieur du bloc). Par exemple :

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}

La voie immuable

Vous vous demandez peut-être pourquoi Hash.new([]) ne fonctionne pas alors que Hash.new(0) fonctionne très bien. La clé est qu'en Ruby, les valeurs numériques sont immuables, et qu'il est donc naturel de ne jamais les modifier en cours de route. Si nous considérions notre valeur par défaut comme immuable, nous pourrions utiliser Hash.new([]) très bien aussi :

h = Hash.new([].freeze)
h[0] += ['a']  #=> ["a"]
h[1] += ['b']  #=> ["b"]
h[2]           #=> []
h              #=> {0=>["a"], 1=>["b"]}

Cependant, notez que ([].freeze + [].freeze).frozen? == false . Ainsi, si vous voulez vous assurer que l'immutabilité est préservée de bout en bout, vous devez prendre soin de regeler le nouvel objet.


Conclusion

De toutes les manières, je préfère personnellement "la manière immuable" - l'immuabilité rend généralement le raisonnement sur les choses beaucoup plus simple. C'est, après tout, la seule méthode qui n'a aucune possibilité de comportement caché ou subtil inattendu. Cependant, la méthode la plus courante et la plus idiomatique est la "méthode mutable".

Pour finir, ce comportement des valeurs par défaut de Hash est noté dans Koans de Rubis .


<sup>† </sup>Ce n'est pas strictement vrai, des méthodes comme <code>instance_variable_set</code> de contourner ce problème, mais ils doivent exister pour la métaprogrammation puisque la valeur l en <code>=</code> ne peut être dynamique.

25voto

Gareth Points 42402

Lorsque vous appelez Hash.new([]) la valeur par défaut de toute clé n'est pas seulement un vide, c'est le même un tableau vide.

Pour créer un nouveau tableau pour chaque valeur par défaut, utilisez la forme bloc du constructeur :

Hash.new { [] }

23voto

Matthew Flaschen Points 131723

Vous spécifiez que la valeur par défaut du hachage est une référence à ce tableau particulier (initialement vide).

Je pense que vous voulez :

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

Cela définit la valeur par défaut de chaque clé à une valeur nouveau le tableau.

0 votes

Comment puis-je utiliser des instances de tableau distinctes pour chaque nouveau hachage ?

5 votes

Cette version bloc vous donne de nouvelles Array à chaque invocation. A savoir : h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570 . Aussi : si vous utilisez la version de bloc qui fixe la valeur ( {|hash,key| hash[key] = []} ) plutôt que celle qui consiste simplement à génère la valeur ( { [] } ), il suffit alors de << pas <<= lors de l'ajout d'éléments.

3voto

L'opérateur += lorsqu'ils sont appliqués à ces hachages fonctionnent comme prévu.

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

Cela peut être dû au fait que foo[bar]+=baz est un sucre syntaxique pour foo[bar]=foo[bar]+baz quand foo[bar] à la droite de = est évaluée, elle renvoie le valeur par défaut et l'objet + L'opérateur ne le changera pas. La main gauche est un sucre syntaxique pour l'opérateur []= qui ne modifiera pas la méthode valeur par défaut .

Notez que cela ne s'applique pas à foo[bar]<<=baz car il sera équivalent à foo[bar]=foo[bar]<<baz y << sera changer le valeur par défaut .

De même, je n'ai trouvé aucune différence entre Hash.new{[]} y Hash.new{|hash, key| hash[key]=[];} . Au moins sur ruby 2.1.2 .

1voto

Ganesh Sagare Points 379

Quand tu écris,

h = Hash.new([])

vous passez la référence par défaut du tableau à tous les éléments du hash. à cause de cela, tous les éléments du hash font référence au même tableau.

si vous voulez que chaque élément du hachage fasse référence à un tableau séparé, vous devez utiliser

h = Hash.new{[]} 

pour plus de détails sur la façon dont cela fonctionne dans ruby, veuillez lire ceci : http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

0 votes

C'est faux, Hash.new { [] } fait no travail. Voir ma réponse pour les détails. C'est aussi déjà la solution proposée dans une autre réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X