Comportamiento extraño e inesperado (desaparición/cambio de valores) cuando se utiliza el valor predeterminado de Hash, por ejemplo, Hash.new([])

Resuelto Valentin V asked hace 14 años • 5 respuestas

Considere este código:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

Todo eso está bien, pero:

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

En este punto espero que el hash sea:

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

pero está lejos de eso. ¿Qué está pasando y cómo puedo obtener el comportamiento que espero?

Valentin V avatar Apr 23 '10 19:04 Valentin V
Aceptado

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. También se aplica de manera similar a los elementos poblados en Array.new(3, []).

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

¿Por qué 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 valor predeterminado nuevo y fresco), pero ¿por qué no hay claves ni valores? en la matriz, a pesar de h[1]todavía darnos un valor? He aquí 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 una asignación en Ruby sin un =presente ), 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')

¿Por qué Hash.new { [] }no funciona?

El uso Hash.new { [] }resuelve el problema de reutilizar y mutar el valor predeterminado original (ya que el bloque dado se llama 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"]}

¿Qué 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 poco idiomática (nunca la he visto <<=usada en la naturaleza). También es propenso a errores sutiles si <<se usa sin darse cuenta.

La manera mutable

La documentación paraHash.new los estados (el énfasis es mío):

Si se especifica un bloque, se llamará con el objeto hash y la clave, y debería devolver el valor predeterminado. Es responsabilidad del bloque almacenar el valor en el hash si es necesario .

Por lo tanto, debemos almacenar el valor predeterminado en el hash dentro del bloque si deseamos usarlo <<en 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 a Hash.new, eliminando la carga del comportamiento inesperado al usar <<.

Tenga en cuenta que existe 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 en Ruby son inmutables, por lo que, naturalmente, nunca terminamos mutándolos in situ. Si tratáramos nuestro valor predeterminado como inmutable, Hash.new([])también podríamos usarlo 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 conserve 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 simplifica mucho el razonamiento sobre las cosas. Después de todo, es el único método que no tiene posibilidad de comportamiento inesperado oculto o sutil. Sin embargo, la forma más común e idiomática es “la forma mutable”.

Como último comentario, este comportamiento de los valores predeterminados de Hash se observa en Ruby Koans .


Esto no es estrictamente cierto, los métodos como instance_variable_setevitan esto, pero deben existir para la metaprogramación ya que el valor l =no puede ser dinámico.

Andrew Marshall avatar Mar 07 '2015 15:03 Andrew Marshall

Estás especificando que el valor predeterminado para el hash es una referencia a esa matriz particular (inicialmente vacía).

Creo que quieres:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

Eso establece el valor predeterminado para cada clave en una nueva matriz.

Matthew Flaschen avatar Apr 23 '2010 12:04 Matthew Flaschen