Directrices GetHashCode en C #

Leí en el libro Essential C # 3.0 y .NET 3.5 que:

Los retornos de GetHashCode () durante la vida de un objeto en particular deben ser constantes (el mismo valor), incluso si los datos del objeto cambian. En muchos casos, debe guardar en caché el método return para aplicar esto.

¿Es esta una guía válida?

He intentado un par de tipos incorporados en .NET y no se comportaron así.

La respuesta es principalmente, es una guía válida, pero tal vez no sea una regla válida. Tampoco cuenta toda la historia.

El punto que se está haciendo es que para los tipos mutables, no se puede basar el código hash en los datos mutables porque dos objetos iguales deben devolver el mismo código hash y el código hash debe ser válido durante el tiempo de vida del objeto. Si el código hash cambia, terminas con un objeto que se pierde en una colección hash porque ya no vive en el hash bin correcto.

Por ejemplo, el objeto A devuelve hash de 1. Por lo tanto, va en el bin 1 de la tabla hash. Luego cambias el objeto A de tal manera que devuelve un hash de 2. Cuando una tabla hash va a buscarlo, busca en el bin 2 y no puede encontrarlo – el objeto está huérfano en el bin 1. Esta es la razón por la cual el código hash debe no cambia durante el tiempo de vida del objeto , y solo una razón por la cual escribir implementaciones de GetHashCode es un problema.

Actualizar
Eric Lippert ha publicado un blog que brinda información excelente sobre GetHashCode .

Actualización adicional
He hecho un par de cambios arriba:

  1. Hice una distinción entre lineamiento y regla.
  2. Ataqué “durante toda la vida del objeto”.

Una guía es solo una guía, no una regla. En realidad, GetHashCode solo tiene que seguir estas pautas cuando las cosas esperan que el objeto siga las pautas, como cuando se almacena en una tabla hash. Si nunca intenta utilizar sus objetos en tablas hash (o cualquier otra cosa que dependa de las reglas de GetHashCode ), su implementación no necesita seguir las pautas.

Cuando vea “durante la vida útil del objeto”, debe leer “por el tiempo que el objeto necesite para cooperar con tablas hash” o similar. Como la mayoría de las cosas, GetHashCode se trata de saber cuándo romper las reglas.

Ha pasado mucho tiempo, pero aún así creo que todavía es necesario dar una respuesta correcta a esta pregunta, incluidas explicaciones sobre los por qué y cómo. La mejor respuesta hasta el momento es la de citar MSDN exhaustivamente, no intente crear sus propias reglas, los tipos de MS sabían lo que estaban haciendo.

Pero primero lo primero: la directriz citada en la pregunta es incorrecta.

Ahora los por qué – hay dos de ellos

Primero, por qué : si el código hash se calcula de una manera que no cambie durante la vida útil de un objeto, incluso si el objeto mismo cambia, entonces rompería el contrato igual.

Recuerde: “Si dos objetos se comparan como iguales, el método GetHashCode para cada objeto debe devolver el mismo valor. Sin embargo, si dos objetos no se pueden comparar como iguales, los métodos GetHashCode para los dos objetos no tienen que devolver valores diferentes”.

La segunda oración a menudo se malinterpreta como “La única regla es que en el momento de creación del objeto, el código hash de objetos iguales debe ser igual”. Realmente no sé por qué, pero esa es la esencia de la mayoría de las respuestas aquí también.

Piense en dos objetos que contienen un nombre, donde el nombre se usa en el método equals: Mismo nombre -> same thing. Crear instancia A: Nombre = Joe Crear instancia B: Nombre = Peter

Hashcode A y Hashcode B probablemente no sean lo mismo. ¿Qué pasaría ahora, cuando el nombre de la instancia B se cambie a Joe?

De acuerdo con la guía de la pregunta, el código hash de B no cambiaría. El resultado de esto sería: A.Equals (B) ==> true Pero al mismo tiempo: A.GetHashCode () == B.GetHashCode () ==> false.

Pero exactamente este comportamiento está prohibido explícitamente por el contrato de igualdad y hashcode.

Segundo por qué : si bien es cierto, por supuesto, que los cambios en el código hash pueden romper las listas hash y otros objetos utilizando el código hash, lo contrario también es cierto. No cambiar el hashcode en el peor de los casos obtendrá listas hash, donde todos los objetos diferentes tendrán el mismo código hash y, por lo tanto, estarán en la misma bandeja hash: esto sucede cuando los objetos se inicializan con un valor estándar, por ejemplo.


Ahora viene el cómo Bueno, a primera vista, parece haber una contradicción: de cualquier manera, el código se romperá. Pero ninguno de los dos problemas proviene de códigos hash modificados o no modificados.

La fuente de los problemas está bien descrita en MSDN:

Desde la entrada de hashtable de MSDN:

Los objetos clave deben ser inmutables siempre que se utilicen como claves en el Hashtable.

Esto significa:

Cualquier objeto que crea un valor hash debe cambiar el valor hash, cuando el objeto cambia, pero no debe, absolutamente no debe, permitir ningún cambio en sí mismo, cuando se usa dentro de una tabla Hash (o cualquier otro objeto que use hash, por supuesto) .

En primer lugar, la forma más sencilla sería, por supuesto, diseñar objetos inmutables solo para el uso en hashtables, que se crearán como copias de lo normal, los objetos mutables cuando sea necesario. Dentro de los objetos inmutables, obviamente está bien guardar en caché el código hash, ya que es inmutable.

En segundo lugar, otorgándole al objeto un “has hashed now” -flag, asegúrese de que todos los datos del objeto sean privados, marque la bandera en todas las funciones que pueden cambiar los datos de los objetos y arroje una excepción si no se permite el cambio (p. Ej. ) Ahora, cuando coloque el objeto en cualquier área de hash, asegúrese de establecer la bandera y, a su vez, desarmar la bandera, cuando ya no sea necesaria. Para facilitar el uso, le aconsejo configurar el indicador de forma automática dentro del método “GetHashCode”, de esta manera no puede olvidarse. Y la llamada explícita de un método “ResetHashFlag” asegurará que el progtwigdor tendrá que pensar, ya sea que esté o no permitido cambiar los datos de los objetos por ahora.

Ok, lo que también debería decirse: hay casos en los que es posible tener objetos con datos mutables, donde el código hash no se modifica, sin embargo, cuando se cambian los datos de los objetos, sin violar el contrato de igualdad y hashcode.

Sin embargo, esto requiere que el método equals no se base también en los datos mutables. Entonces, si escribo un objeto y creo un método GetHashCode que calcule un valor solo una vez y lo almacene dentro del objeto para devolverlo a llamadas posteriores, entonces debo, nuevamente: absolutamente debo, crear un método Equals, que use valores almacenados para la comparación, de modo que A.Equals (B) nunca cambiará de falso a verdadero también. De lo contrario, el contrato se rompería. El resultado de esto generalmente será que el método Equals no tiene ningún sentido, no es la referencia original igual, pero tampoco es un valor igual. A veces, esto puede ser un comportamiento intencionado (es decir, registros de clientes), pero por lo general no lo es.

Entonces, simplemente haga que el resultado de GetHashCode cambie, cuando los datos del objeto cambien, y si el uso del objeto dentro del hash usando listas u objetos es intencionado (o simplemente posible) entonces haga que el objeto sea inmutable o cree un indicador de solo lectura para usar para el tiempo de vida de una lista hash que contiene el objeto.

(Por cierto: todo esto no es específico de C # oder .NET – está en la naturaleza de todas las implementaciones de hashtable, o más generalmente de cualquier lista indexada, que la identificación de datos de objetos nunca debería cambiar, mientras el objeto está en la lista Se producirá un comportamiento imprevisible e imprevisible si esta regla se rompe. En algún lugar, puede haber implementaciones de lista, que monitorean todos los elementos dentro de la lista y reindexan automáticamente la lista, pero el desempeño de estos seguramente será espantoso en el mejor de los casos).

Desde MSDN

Si dos objetos se comparan como iguales, el método GetHashCode para cada objeto debe devolver el mismo valor. Sin embargo, si dos objetos no se pueden comparar como iguales, los métodos GetHashCode para los dos objetos no tienen que devolver valores diferentes.

El método GetHashCode para un objeto debe devolver consistentemente el mismo código hash siempre que no haya ninguna modificación en el estado del objeto que determine el valor de retorno del método Equals del objeto. Tenga en cuenta que esto es cierto solo para la ejecución actual de una aplicación, y que se puede devolver un código hash diferente si la aplicación se ejecuta nuevamente.

Para obtener el mejor rendimiento, una función hash debe generar una distribución aleatoria para todas las entradas.

Esto significa que si el / los valor (es) del objeto cambian, el código hash debería cambiar. Por ejemplo, una clase “Persona” con la propiedad “Nombre” configurada como “Tom” debe tener un código hash y un código diferente si cambia el nombre a “Jerry”. De lo contrario, Tom == Jerry, que probablemente no sea lo que hubieras querido.


Editar :

También desde MSDN:

Las clases derivadas que anulan GetHashCode también deben anular Equals para garantizar que dos objetos considerados iguales tengan el mismo código hash; de lo contrario, el tipo Hashtable podría no funcionar correctamente.

Desde la entrada de hashtable de MSDN :

Los objetos clave deben ser inmutables siempre que se utilicen como claves en el Hashtable.

La forma en que leo esto es que los objetos mutables deberían devolver códigos hash diferentes a medida que cambian sus valores, a menos que estén diseñados para usarlos en una tabla hash.

En el ejemplo de System.Drawing.Point, el objeto es mutable y devuelve un código hash diferente cuando cambia el valor X o Y. Esto lo haría un candidato pobre para ser usado como está en una tabla hash.

Creo que la documentación sobre GetHashcode es un poco confusa.

Por un lado, MSDN establece que el código hash de un objeto nunca debe cambiar y ser constante. Por otro lado, MSDN también establece que el valor de retorno de GetHashcode debe ser igual para 2 objetos, si esos dos objetos se consideran iguales.

MSDN:

Una función hash debe tener las siguientes propiedades:

  • Si dos objetos se comparan como iguales, el método GetHashCode para cada objeto debe devolver el mismo valor. Sin embargo, si dos objetos no se pueden comparar como iguales, los métodos GetHashCode para los dos objetos no tienen que devolver valores diferentes.
  • El método GetHashCode para un objeto debe devolver consistentemente el mismo código hash siempre que no haya ninguna modificación en el estado del objeto que determine el valor de retorno del método Equals del objeto. Tenga en cuenta que esto es cierto solo para la ejecución actual de una aplicación, y que se puede devolver un código hash diferente si la aplicación se ejecuta nuevamente.
  • Para obtener el mejor rendimiento, una función hash debe generar una distribución aleatoria para todas las entradas.

Entonces, esto significa que todos sus objetos deben ser inmutables, o el método GetHashcode debe basarse en las propiedades de su objeto que son inmutables. Supongamos, por ejemplo, que tienes esta clase (implementación ingenua):

 public class SomeThing { public string Name {get; set;} public override GetHashCode() { return Name.GetHashcode(); } public override Equals(object other) { SomeThing = other as Something; if( other == null ) return false; return this.Name == other.Name; } } 

Esta implementación ya infringe las reglas que se pueden encontrar en MSDN. Supongamos que tiene 2 instancias de esta clase; la propiedad Name de instance1 se establece en ‘Pol’, y la propiedad Name de instance2 se establece en ‘Piet’. Ambas instancias devuelven un código hash diferente y tampoco son iguales. Supongamos ahora que cambio el Nombre de instancia2 por ‘Pol’, luego, de acuerdo con mi método Equals, ambas instancias deben ser iguales y, de acuerdo con una de las reglas de MSDN, deben devolver el mismo código hash.
Sin embargo, esto no se puede hacer, ya que el hashcode de instance2 cambiará y MSDN indicará que esto no está permitido.

Entonces, si tiene una entidad, podría implementar el código hash para que use el “identificador primario” de esa entidad, que es idealmente una clave sustituta, o una propiedad inmutable. Si tiene un objeto de valor, puede implementar Hashcode para que use las ‘propiedades’ de ese objeto de valor. Esas propiedades conforman la ‘definición’ del objeto de valor. Esta es, por supuesto, la naturaleza de un objeto de valor; no le interesa su identidad, sino su valor.
Y, por lo tanto, los objetos de valor deben ser inmutables. (Al igual que en .NET framework, string, Date, etc … son todos objetos inmutables).

Otra cosa que viene en mente:
Durante el cual ‘sesión’ (no sé realmente cómo debería llamar esto) debería ‘GetHashCode’ devolver un valor constante. Supongamos que abre su aplicación, carga una instancia de un objeto fuera del DB (una entidad) y obtiene su código hash. Devolverá un cierto número. Cierre la aplicación y cargue la misma entidad. ¿Se requiere que el código hash esta vez tenga el mismo valor que cuando cargó la entidad la primera vez? En mi humilde opinión, no.

Este es un buen consejo. Esto es lo que Brian Pepin tiene que decir al respecto:

Esto me ha hecho tropezar más de una vez: asegúrese de que GetHashCode siempre devuelva el mismo valor a lo largo de la vida de una instancia. Recuerde que los códigos hash se utilizan para identificar “cubos” en la mayoría de las implementaciones de hashtable. Si el “cubo” de un objeto cambia, una tabla hash puede no ser capaz de encontrar su objeto. Estos pueden ser errores muy difíciles de encontrar, así que hazlo bien la primera vez.

No responde directamente su pregunta, pero si usa Resharper, no olvide que tiene una función que genera una implementación GetHashCode razonable (así como el método Equals) para usted. Por supuesto, puede especificar qué miembros de la clase se tendrán en cuenta al calcular el código hash.

Echa un vistazo a esta publicación de blog de Marc Brooks:

VTOs, RTOs y GetHashCode () – ¡oh, Dios mío!

Y luego revise la publicación de seguimiento (no se puede vincular porque soy nuevo, pero hay un enlace en el artículo de initlal) que trata más adelante y cubre algunas debilidades menores en la implementación inicial.

Esto era todo lo que necesitaba saber sobre la creación de una implementación de GetHashCode (), incluso proporciona una descarga de su método junto con otras utilidades, en oro corto.

El código hash nunca cambia, pero también es importante entender de dónde viene Hashcode.

Si su objeto usa semántica de valores, es decir, la identidad del objeto está definida por sus valores (como String, Color, all structs). Si la identidad de su objeto es independiente de todos sus valores, entonces Hashcode se identifica por un subconjunto de sus valores. Por ejemplo, su entrada de StackOverflow está almacenada en alguna base de datos. Si cambia su nombre o correo electrónico, la entrada de su cliente se mantiene igual, aunque algunos valores han cambiado (en última instancia, generalmente se identifica con un ID de cliente largo).

En resumen:

Semántica del tipo de valor: Hashcode está definido por valores Semántica del tipo de referencia: Hashcode está definido por algún id.

Le sugiero que lea Domain Driven Design por Eric Evans, donde va a las entidades frente a los tipos de valores (que es más o menos lo que intenté hacer arriba) si esto todavía no tiene sentido.

Consulte las pautas y reglas para GetHashCode por Eric Lippert