Comportement étrange et inattendu (disparition / modification de valeurs) lors de l’utilisation de la valeur par défaut de 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} 

Tout va bien, mais:

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

À ce stade, je m’attends à ce que le hash soit:

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

mais c’est loin d’être ça. Qu’est-ce qui se passe et comment puis-je obtenir le comportement que j’attends?

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), et pas uniquement les tableaux.

TL; DR : Utilisez Hash.new { |h, k| h[k] = [] } Hash.new { |h, k| h[k] = [] } si vous voulez la solution la plus simple et la plus idiomatique.


Ce qui ne fonctionne 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 object par défaut est en train d’être 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 dans le tableau, bien que h[1] nous donne toujours une valeur? Voici un indice:

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

Le tableau renvoyé par chaque appel [] est simplement la valeur par défaut, que nous avons mutée pendant tout ce temps, et contient maintenant nos nouvelles valeurs. Puisque < < ne pas assigner au hachage (il ne peut jamais y avoir d'affectation dans Ruby sans un = 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 (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 rappelons 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 bogues subtils si < < est utilisé par inadvertance.

La voie mutable

La documentation pour les états de Hash.new (accent mis par moi-même):

Si un bloc est spécifié, il sera appelé avec l'object hash 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 depuis le 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 effectivement l'affectation de nos appels individuels (qui utiliseraient < <= ) au bloc passé à Hash.new , supprimant ainsi le fardeau d'un comportement inattendu lors de l'utilisation de < < .

Notez qu'il existe une différence fonctionnelle entre cette méthode et les autres: cette méthode assigne la valeur par défaut lors de 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 manière immuable

Vous vous demandez peut-être pourquoi Hash.new([]) ne fonctionne pas alors que Hash.new(0) fonctionne Hash.new(0) . L'essentiel est que Numerics in Ruby est immuable, de sorte que nous ne les transformons jamais naturellement en place. Si nous traitions notre valeur par défaut comme immuable, nous pourrions utiliser Hash.new([]) très bien également:

 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 ([].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 recréer le nouvel object.

De toutes les manières, je préfère personnellement cette manière - l'immutabilité rend généralement le raisonnement sur des choses beaucoup plus simple (c'est, après tout, la seule méthode qui n'a aucune possibilité de comportement inattendu, caché ou subtil).


Ce n'est pas tout à fait vrai, les méthodes telles que instance_variable_set contournent cela, mais elles doivent exister pour la métaprogrammation car la valeur l dans = ne peut pas être dynamic.

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

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

 Hash.new { [] } 

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é pour un nouveau tableau.

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]} 

C’est peut-être parce que foo[bar]+=baz est un sucre syntaxique pour foo[bar]=foo[bar]+baz lorsque foo[bar] à droite de = est évalué, il retourne l’object valeur par défaut et l’opérateur + pas le changer La main gauche est le sucre syntaxique pour la méthode []= qui ne changera pas la valeur par défaut .

Notez que cela ne s’applique pas à foo[bar]< <=baz car il sera équivalent à foo[bar]=foo[bar]< et < < changeront la valeur par défaut .

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

Quand 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 dans le hachage font référence au même tableau.

Si vous voulez que chaque élément du hachage se réfère à un tableau séparé, vous devez utiliser

 h = Hash.new{[]} 

Pour plus de détails sur son fonctionnement dans Ruby, veuillez passer par ceci: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new