Primero, tenga en cuenta que este comportamiento se aplica a cualquier valor predeterminado que se mute posteriormente (por ejemplo, hashes y cadenas), no solo a las matrices.
TL; DR : Úselo Hash.new { |h, k| h[k] = [] }
si desea la solución más idiomática y no le importa por qué.
Lo que no funciona
Porque Hash.new([])
no funciona
Veamos más en profundidad por qué Hash.new([])
no funciona:
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 #=> {}
Podemos ver que nuestro objeto predeterminado se está reutilizando y mutando (esto se debe a que se pasa como el único valor predeterminado, el hash no tiene forma de obtener un nuevo valor predeterminado nuevo), pero ¿por qué no hay claves o valores? en la matriz, a pesar de que h[1]
todavía nos da un valor? Aquí hay una pista:
h[42] #=> ["a", "b"]
La matriz devuelta por cada []
llamada es solo el valor predeterminado, que hemos estado mutando todo este tiempo, por lo que ahora contiene nuestros nuevos valores. Dado <<
que no se asigna al hash (nunca puede haber asignación en Ruby sin un =
regalo † ), nunca hemos puesto nada en nuestro hash real. En su lugar, tenemos que usar <<=
(que es <<
como +=
es +
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
Esto es lo mismo que:
h[2] = (h[2] << 'c')
Porque Hash.new { [] }
no funciona
Usar Hash.new { [] }
resuelve el problema de reutilizar y mutar el valor predeterminado original (como se llama al bloque dado cada vez, devolviendo una nueva matriz), pero no el problema de asignación:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
Que funciona
La forma de asignación
Si recordamos usar siempre <<=
, entonces Hash.new { [] }
es una solución viable, pero es un poco extraña y no idiomática (nunca la he visto <<=
usada en la naturaleza). También es propenso a errores sutiles si <<
se usa inadvertidamente.
La forma mutable
los documentación de losHash.new
estados (el énfasis es mío):
Si se especifica un bloque, se llamará con el objeto hash y la clave, y debe devolver el valor predeterminado. Es responsabilidad del bloque almacenar el valor en el hash si es necesario .
Así que debemos almacenar el valor predeterminado en el hash desde dentro del bloque si deseamos usar <<
lugar de <<=
:
h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}
Esto efectivamente mueve la asignación de nuestras llamadas individuales (que usarían <<=
) al bloque pasado Hash.new
, eliminando la carga del comportamiento inesperado al usar <<
.
Tenga en cuenta que hay una diferencia funcional entre este método y los demás: de esta manera asigna el valor predeterminado al leer (ya que la asignación siempre ocurre dentro del bloque). Por ejemplo:
h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}
h2 = Hash.new { [] }
h2[:x]
h2 #=> {}
El camino inmutable
Quizás se pregunte por qué Hash.new([])
no funciona mientras Hash.new(0)
funciona bien. La clave es que los números numéricos en Ruby son inmutables, por lo que, naturalmente, nunca terminamos por mutarlos en el lugar. Si tratamos nuestro valor predeterminado como inmutable, también podríamos usarlo Hash.new([])
bien:
h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}
Sin embargo, tenga en cuenta que ([].freeze + [].freeze).frozen? == false
. Por lo tanto, si desea asegurarse de que la inmutabilidad se mantenga en todo momento, debe tener cuidado de volver a congelar el nuevo objeto.
Conclusión
De todas las formas, personalmente prefiero “la forma inmutable”; la inmutabilidad generalmente hace que el razonamiento sobre las cosas sea mucho más simple. Después de todo, es el único método que no tiene posibilidad de un comportamiento inesperado oculto o sutil. Sin embargo, la forma más común e idiomática es "la forma mutable".
Como comentario final, este comportamiento de los valores predeterminados de Hash se observa en Ruby Koans .
† Esto no es estrictamente cierto, métodos como instance_variable_set
eludir esto, pero deben existir para la metaprogramación ya que el valor l de =
no puede ser dinámico.