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.