ruby string hash mutable

ruby - ¿Por qué se congela una clave de cadena para un hash?



string mutable (4)

En resumen, es solo Ruby tratando de ser amable.

Cuando se ingresa una clave en una Hash, se calcula un número especial, usando el método hash de la tecla. El objeto Hash usa este número para recuperar la clave. Por ejemplo, si pregunta cuál es el valor de h[''a''] , el hash llama al método hash de la cadena ''a'' y verifica si tiene un valor almacenado para ese número. El problema surge cuando alguien (tú) muta el objeto cadena, por lo que la cadena ''a'' es ahora otra cosa, digamos ''aa''. The Hash no encontraría un número hash para ''aa''.

Los tipos más comunes de claves para hashes son cadenas, símbolos y números enteros. Los símbolos y los enteros son inmutables, pero las cadenas no lo son. Ruby intenta protegerte del comportamiento confuso descrito anteriormente al duplicar y congelar las claves de cadena. Supongo que no está hecho para otros tipos porque podría haber efectos secundarios desagradables (piense en arreglos grandes).

De acuerdo con la specification , las cadenas que se utilizan como clave para un hash se duplican y congelan. Otros objetos mutables no parecen tener una consideración tan especial. Por ejemplo, con una clave de matriz, lo siguiente es posible.

a = [0] h = {a => :a} h.keys.first[0] = 1 h # => {[1] => :a} h[[1]] # => nil h.rehash h[[1]] # => :a

Por otro lado, algo similar no se puede hacer con una clave de cadena.

s = "a" h = {s => :s} h.keys.first.upcase! # => RuntimeError: can''t modify frozen String

¿Por qué la cuerda está diseñada para ser diferente de otros objetos mutables cuando se trata de una clave hash? ¿Hay algún caso de uso donde esta especificación se vuelva útil? ¿Qué otras consecuencias tiene esta especificación?

De hecho, tengo un caso de uso en el que la ausencia de tal especificación especial sobre cadenas puede ser útil. Es decir, leí con yaml gem un archivo YAML escrito manualmente que describe un hash. las claves pueden ser cadenas, y me gustaría permitir la insensibilidad de mayúsculas y minúsculas en el archivo original de YAML. Cuando leo un archivo, podría obtener un hash como este:

h = {"foo" => :foo, "Bar" => :bar, "BAZ" => :baz}

Y quiero normalizar las teclas de la letra minúscula para obtener esto:

h = {"foo" => :foo, "bar" => :bar, "baz" => :baz}

haciendo algo como esto:

h.keys.each(&:downcase!)

pero eso devuelve un error por la razón explicada anteriormente.


Estás haciendo 2 preguntas diferentes: teórico y práctico. Lain fue el primero en responder, pero me gustaría brindarle lo que considero una solución adecuada y más perezosa a su pregunta práctica:

Hash.new { |hsh, key| # this block get''s called only if a key is absent downcased = key.to_s.downcase unless downcased == key # if downcasing makes a difference hsh[key] = hsh[downcased] if hsh.has_key? downcased # define a new hash pair end # (otherways just return nil) }

El bloque utilizado con Hash.new constructor solo se invoca para aquellas claves que faltan, que en realidad se solicitan. La solución anterior también acepta símbolos.


Las claves inmutables tienen sentido en general porque sus códigos hash serán estables.

Esta es la razón por la cual las cadenas se convierten especialmente, en esta parte del código de MRI:

if (RHASH(hash)->ntbl->type == &identhash || rb_obj_class(key) != rb_cString) { st_insert(RHASH(hash)->ntbl, key, val); } else { st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); }

En pocas palabras, en el caso de la clave de cadena, a st_insert2 se le pasa un puntero a una función que disparará el dup y se congelará.

Entonces, si teóricamente quisiéramos admitir listas inmutables y hashes inmutables como claves hash, entonces podríamos modificar ese código a algo como esto:

VALUE key_klass; key_klass = rb_obj_class(key); if (key_klass == rb_cArray || key_klass == rb_cHash) { st_insert2(RHASH(hash)->ntbl, key, val, freeze_obj); } else if (key_klass == rb_cString) { st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); } else { st_insert(RHASH(hash)->ntbl, key, val); }

Donde freeze_obj se definiría como:

static st_data_t freeze_obj(st_data_t obj) { return (st_data_t)rb_obj_freeze((VALUE) obj); }

Entonces eso resolvería la inconsistencia específica que observaste, donde la matriz de teclas era mutable. Sin embargo, para ser realmente consistente, también habría que hacer más inmutables otros tipos de objetos.

No todos los tipos, sin embargo. Por ejemplo, no tiene sentido congelar objetos inmediatos como Fixnum porque efectivamente solo hay una instancia de Fixnum correspondiente a cada valor entero. Esta es la razón por la cual solo String necesita ser encapsulado de esta manera, no Fixnum y Symbol .

Las cadenas son una excepción especial por cuestiones de conveniencia para los programadores de Ruby, ya que las cadenas se utilizan a menudo como claves hash.

Por el contrario, la razón por la que otros tipos de objetos no están congelados de esta manera, lo que sin duda conduce a un comportamiento incoherente, es sobre todo una cuestión de conveniencia para Matz & Company para no admitir casos límite. En la práctica, comparativamente pocas personas usarán un objeto contenedor como una matriz o un hash como una clave hash. Entonces, si lo hace, le corresponde a usted congelar antes de la inserción.

Tenga en cuenta que esto no se trata estrictamente de rendimiento, porque el acto de congelar un objeto no inmediato simplemente implica voltear el bit FL_FREEZE en el basic.flags bits basic.flags que está presente en cada objeto. Eso es, por supuesto, una operación barata.

Hablando también de rendimiento, tenga en cuenta que si va a utilizar claves de cadena, y se encuentra en una sección de código de rendimiento crítico, es posible que desee congelar las cadenas antes de realizar la inserción. Si no lo hace, se activa un dup, que es una operación más costosa.

Update @sawa señaló que dejar su matriz simplemente congelada significa que la matriz original podría ser inesperadamente inmutable fuera del contexto de uso de la clave, lo que también podría ser una sorpresa desagradable (aunque podría ser útil para usar una matriz como hash-key, realmente). Si, por lo tanto, supone que dup + freeze es la salida de eso, entonces de hecho incurriría en un posible costo de rendimiento notable. En tercer lugar, déjalo sin descongelar por completo, y obtienes la rareza original del OP. La rareza alrededor. Otra razón para Matz et al para diferir estos casos extremos para el programador.


Vea este hilo en la lista de correo de ruby-core para obtener una explicación (extrañamente, resultó ser el primer correo con el que tropecé cuando abrí la lista de correo en mi aplicación de correo).

No tengo idea acerca de la primera parte de su pregunta, pero h Aquí hay una respuesta práctica para la segunda parte:

new_hash = {} h.each_pair do |k,v| new_hash.merge!({k.downcase => v}) end h.replace new_hash

Hay muchas permutaciones de este tipo de código,

Hash[ h.map{|k,v| [k.downcase, v] } ]

ser otro (y probablemente estés al tanto de esto, pero a veces es mejor tomar la ruta práctica :)