Comportamiento extraño e inesperado (valores que desaparecen / cambian) cuando se usa el valor predeterminado de Hash, por ejemplo, Hash.new ([])

107

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}

Está todo 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á sucediendo y cómo puedo obtener el comportamiento que espero?

Valentin Vasilyev
fuente

Respuestas:

164

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_seteludir esto, pero deben existir para la metaprogramación ya que el valor l de =no puede ser dinámico.

Andrew Marshall
fuente
1
Vale la pena mencionar que el uso de "la forma mutable" también tiene el efecto de hacer que cada búsqueda de hash almacene un par de valores clave (ya que hay una asignación en el bloque), lo que puede no ser siempre deseable.
johncip
@johncip No todas las búsquedas, solo la primera de cada clave. Pero veo lo que quieres decir, lo agregaré a la respuesta más adelante; ¡Gracias!.
Andrew Marshall
Ups, siendo descuidado. Tiene razón, por supuesto, es la primera búsqueda de una clave desconocida. Casi siento que { [] }con <<=tiene la menor cantidad de sorpresas, si no fuera por el hecho de que olvidarlo accidentalmente =podría llevar a una sesión de depuración muy confusa.
Johncip
Explicaciones bastante claras sobre las diferencias al inicializar hash con valores predeterminados
cisolarix
23

Está especificando que el valor predeterminado para el hash es una referencia a esa matriz en 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
fuente
¿Cómo puedo usar instancias de matriz separadas para cada nuevo hash?
Valentin Vasilyev
5
Esa versión de bloque le brinda nuevas Arrayinstancias en cada invocación. A saber: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Además: si usa la versión de bloque que establece el valor ( {|hash,key| hash[key] = []}) en lugar de la que simplemente genera el valor ( { [] }), entonces solo necesita <<, no <<=al agregar elementos.
James A. Rosen
3

El operador +=cuando se aplica a esos hashes funciona como se esperaba.

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

Esto puede deberse a que foo[bar]+=bazes un azúcar sintáctico porque foo[bar]=foo[bar]+bazcuando se evalúa foo[bar]a la derecha de =, devuelve el objeto de valor predeterminado y el +operador no lo cambiará. La mano izquierda es azúcar sintáctica para el []=método que no cambiará el valor predeterminado .

Tenga en cuenta que esto no se aplica a foo[bar]<<=bazlo que va a ser equivalente a foo[bar]=foo[bar]<<baz, y << será cambiar el valor por defecto .

Además, no encontré ninguna diferencia entre Hash.new{[]}y Hash.new{|hash, key| hash[key]=[];}. Al menos en ruby ​​2.1.2.

Daniel Ribeiro Moreira
fuente
Buena explicación. Parece que en ruby ​​2.1.1 Hash.new{[]}es lo mismo que Hash.new([])para mí con la falta del <<comportamiento esperado (aunque, por supuesto, Hash.new{|hash, key| hash[key]=[];}funciona). Cosas pequeñas extrañas que rompen todas las cosas: /
butterywombat
1

Cuando escribes,

h = Hash.new([])

pasa la referencia predeterminada de la matriz a todos los elementos en hash. por eso todos los elementos en hash se refieren a la misma matriz.

si desea que cada elemento en hash se refiera a una matriz separada, debe usar

h = Hash.new{[]} 

para obtener más detalles sobre cómo funciona en ruby, consulte esto: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Ganesh Sagare
fuente
Esto está mal, Hash.new { [] }no no funciona. Vea mi respuesta para más detalles. También es ya la solución propuesta en otra respuesta.
Andrew Marshall