web-dev-qa-db-fra.com

Comportement étrange et inattendu (disparition / modification des valeurs) lors de l'utilisation de la valeur par défaut de Hash, par ex. 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`?

À 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?

101
Valentin

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 : utilisez Hash.new { |h, k| h[k] = [] } Si vous voulez la solution la plus idiomatique et que vous vous en fichez.


Qu'est-ce qui ne fonctionne 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 transmis 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é que h[1] nous donne toujours 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 muté tout ce temps, donc contient maintenant nos nouvelles valeurs. Étant donné que << N'est pas affecté au hachage (il ne peut jamais y avoir d'affectation dans Ruby sans = Présent)), nous n'avons jamais rien mis dans notre hachage réel. Au lieu de cela, nous devons utiliser <<= (Qui est à << Comme += 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 de Hash.new { [] } Résout le problème de la réutilisation et de la mutation de la valeur par défaut d'origine (car le bloc donné est appelé à chaque fois, en 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 { [] } c'est une solution viable, mais c'est un peu étrange et non idiomatique (je n'ai jamais vu <<= Utilisé à l'état sauvage). Il est également sujet à des bogues subtils si << Est utilisé par inadvertance.

La voie mutable

Le documentation pour Hash.new indique (en mettant l'accent sur le mien):

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 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 à l'intérieur du bloc si nous souhaitons utiliser << Au lieu 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'affectation de nos appels individuels (qui utiliseraient <<=) Vers le bloc passé à Hash.new, Supprimant ainsi le fardeau d'un comportement inattendu lors de l'utilisation de <<.

Notez qu'il y a une différence fonctionnelle entre cette méthode et les autres: cette façon affecte la valeur par défaut à 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 Hash.new(0) fonctionne très bien. La clé est que les nombres en Ruby sont immuables, donc nous ne finissons naturellement jamais par les muter sur place. Si nous avons traité 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 tout au long, vous devez prendre soin de recongeler le nouvel objet.


Conclusion

De toutes les façons, 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 voie la plus courante et 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_set Contournent cela, mais elles doivent exister pour la métaprogrammation car la valeur l dans = Ne peut pas être dynamique.

160
Andrew Marshall

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 tableau nouvea.

23
Matthew Flaschen

L'opérateur += 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 que foo[bar]+=baz est du sucre syntaxique pour foo[bar]=foo[bar]+baz quand foo[bar] à droite de = est évalué, il renvoie l'objet valeur par défaut et l'objet + L'opérateur ne le modifie pas. La main gauche est du sucre syntaxique pour le []= méthode qui ne changera pas la valeur par défaut .

Notez que cela ne s'applique pas à foo[bar]<<=bazcomme cela sera équivalent à foo[bar]=foo[bar]<<baz et <<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 Ruby 2.1.2.

3

Lorsque vous écrivez,

h = Hash.new([])

vous transmettez la référence par défaut du tableau à tous les éléments du hachage. à cause de cela, tous les éléments du 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 la façon dont cela fonctionne dans Ruby veuillez passer par ceci: http://Ruby-doc.org/core-2.2.0/Array.html#method-c -nouvea

1
Ganesh Sagare