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_set
contourner cela, mais elles doivent exister pour la métaprogrammation car la valeur l dans =
ne peut pas être dynamique.