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

107

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}

Tout va 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`?

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

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

mais c'est loin de là. Que se passe-t-il et comment puis-je obtenir le comportement que j'attends?

Valentin Vasilyev
la source

Réponses:

164

Tout d'abord, notez que ce comportement s'applique à toute valeur par défaut qui est ensuite mutée (par exemple, les hachages et les chaînes), pas seulement les tableaux.

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


Ce qui ne marche pas

Pourquoi Hash.new([])ne fonctionne pas

Regardons 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 hachage n'a aucun moyen d'obtenir une nouvelle valeur par défaut), mais pourquoi n'y a-t-il pas de clés ou de valeurs dans le tableau, malgré h[1]toujours nous donner une valeur? Voici un indice:

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

Le tableau renvoyé par chaque []appel n'est que la valeur par défaut, que nous avons muée tout ce temps et qui contient maintenant nos nouvelles valeurs. Puisque <<n'affecte pas au hachage (il ne peut jamais y avoir d'affectation dans Ruby sans =cadeau ), nous n'avons jamais rien mis dans notre hachage réel. Nous avons plutôt à utiliser <<=( ce qui est <<en +=est à +):

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

L'utilisation Hash.new { [] }résout le problème de la réutilisation et de la mutation de la valeur par défaut d'origine (comme 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"]}

Qu'est-ce qui fonctionne

La manière d'affectation

Si nous nous souvenons de toujours utiliser <<=, alors Hash.new { [] } est une solution viable, mais c'est un peu étrange et non idiomatique (je n'ai jamais vu <<=utilisé dans la nature). Il est également sujet à des bugs subtils s'il <<est utilisé par inadvertance.

La manière mutable

La documentation pour lesHash.new états (c'est moi qui souligne):

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

Nous devons donc stocker la valeur par défaut dans le hachage à partir du bloc si nous souhaitons utiliser à la <<place de <<=:

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

Cela déplace efficacement l'assignation de nos appels individuels (qui utiliseraient <<=) vers le bloc passé à Hash.new, supprimant ainsi le fardeau du comportement inattendu lors de l'utilisation <<.

Notez qu'il existe une différence fonctionnelle entre cette méthode et les autres: de cette manière, la valeur par défaut est attribuée à la lecture (car l'affectation se produit 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 cela Hash.new(0)fonctionne très bien. La clé est que les nombres dans Ruby sont immuables, donc nous ne finissons naturellement jamais par les muter sur place. Si nous traitons 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. Donc, si vous voulez vous assurer que l'immuabilité est préservée partout, vous devez prendre soin de recongeler le nouvel objet.


Conclusion

De toutes les manières, je préfère personnellement «la voie 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 inattendu caché ou subtil. Cependant, la manière la plus courante et la plus idiomatique est «la voie mutable».

Enfin , ce comportement des valeurs par défaut de Hash est noté dans Ruby Koans .


Ce n'est pas strictement vrai, des méthodes comme instance_variable_setcontourner cela, mais elles doivent exister pour la métaprogrammation car la valeur l dans =ne peut pas être dynamique.

Andrew Marshall
la source
1
Il convient de mentionner que l'utilisation de "la manière mutable" a également pour effet de faire en sorte que chaque recherche de hachage stocke une paire clé / valeur (car il y a une affectation en cours dans le bloc), ce qui n'est pas toujours souhaité.
johncip
@johncip Pas toutes les recherches, juste la première de chaque clé. Mais je vois ce que vous voulez dire, j'ajouterai cela à la réponse plus tard; Merci!.
Andrew Marshall
Oups, être bâclé. Vous avez raison, bien sûr, c'est la première recherche d'une clé inconnue. J'ai presque l'impression { [] }d' <<=avoir le moins de surprises, =n'eût été du fait que l'oubli accidentel du pourrait conduire à une session de débogage très déroutante.
johncip
explications assez claires sur les différences lors de l'initialisation du hachage avec les valeurs par défaut
cisolarix
23

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

Je pense que tu veux:

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

Cela définit la valeur par défaut de chaque clé sur un nouveau tableau.

Matthew Flaschen
la source
Comment puis-je utiliser des instances de tableau distinctes pour chaque nouveau hachage?
Valentin Vasilyev
5
Cette version de bloc vous donne de nouvelles Arrayinstances à chaque appel. À 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 définit la valeur ( {|hash,key| hash[key] = []}) plutôt que celle qui génère simplement la valeur ( { [] }), alors vous n'en avez besoin que <<, pas <<=lors de l'ajout d'éléments.
James A. Rosen
3

L'opérateur +=lorsqu'il est appliqué à ces hachages fonctionne 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 qu'il foo[bar]+=bazs'agit d'un sucre syntaxique pour, foo[bar]=foo[bar]+bazlorsqu'il est évalué à foo[bar]droite, =il renvoie l' objet de valeur par défaut et l' +opérateur ne le changera pas. La main gauche est le sucre syntaxique pour la []=méthode qui ne changera pas la valeur par défaut .

Notez que cela ne vaut pas foo[bar]<<=bazque ce sera équivalent à foo[bar]=foo[bar]<<bazet << va changer la valeur par défaut .

De plus, je n'ai trouvé aucune différence entre Hash.new{[]}et Hash.new{|hash, key| hash[key]=[];}. Au moins sur le rubis 2.1.2.

Daniel Ribeiro Moreira
la source
Belle explication. Il semble que sur ruby ​​2.1.1 Hash.new{[]}est le même que Hash.new([])pour moi avec le manque de <<comportement attendu (bien que cela Hash.new{|hash, key| hash[key]=[];}fonctionne bien sûr ). De petites choses étranges brisant toutes les choses: /
butterywombat
1

Quand tu écris,

h = Hash.new([])

vous passez la référence par défaut du tableau à tous les éléments du hachage. à cause de cela, tous les éléments de hachage 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 le fonctionnement de ruby, veuillez consulter ceci: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Ganesh Sagare
la source
Ceci est faux, Hash.new { [] }ne fonctionne pas . Voir ma réponse pour plus de détails. C'est aussi déjà la solution proposée dans une autre réponse.
Andrew Marshall