Pautas de GetHashCode en C #

136

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 almacenar en caché el método return para aplicar esto.

¿Es esta una directriz válida?

He probado un par de tipos integrados en .NET y no se comportaron así.

Joan Venge
fuente
Es posible que desee considerar cambiar la respuesta aceptada, si es posible.
Giffyguy

Respuestas:

93

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

El punto que se destaca es que para los tipos mutables, no 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 la vida útil 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 un hash de 1. Por lo tanto, va en el bin 1 de la tabla hash. Luego cambia el objeto A de modo que devuelva un hash de 2. Cuando una tabla hash va a buscarlo, busca en el contenedor 2 y no puede encontrarlo; el objeto queda huérfano en el contenedor 1. Por eso el código hash debe no cambia durante la vida útil del objeto , y solo una de las razones por las cuales escribir implementaciones GetHashCode es una molestia.

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

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

  1. Hice una distinción entre pauta y regla.
  2. Lo atravesé "durante toda la vida del objeto".

Una directriz es solo una guía, no una regla. En realidad, GetHashCodesolo 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 tiene la intención de usar sus objetos en tablas hash (o cualquier otra cosa que se base en las reglas de GetHashCode), su implementación no necesita seguir las pautas.

Cuando vea "durante la vida útil del objeto", debería leer "durante el tiempo que el objeto necesita cooperar con tablas hash" o similar. Como la mayoría de las cosas, GetHashCodese trata de saber cuándo romper las reglas.

Jeff Yates
fuente
1
¿Cómo se determina la igualdad entre los tipos mutables?
Jon B
9
No deberías usar GetHashCode para determinar la igualdad.
JSB ձոգչ
44
@JS Bangs - 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.
Jon B
3
@Joan Venge: dos cosas. Primero, ni siquiera Microsoft tiene GetHashCode correcto en cada implementación. En segundo lugar, los tipos de valor son generalmente inmutables y cada valor es una instancia nueva en lugar de una modificación de una instancia existente.
Jeff Yates el
17
Como a.Equals (b) debe significar que a.GetHashCode () == b.GetHashCode (), el código hash a menudo tiene que cambiar si los datos utilizados para la comparación de igualdad cambian. Diría que el problema no es que GetHashCode se base en datos mutables. El problema es usar objetos mutables como claves de tabla hash (y en realidad mutarlos). ¿Me equivoco?
Niklas
120

Ha pasado mucho tiempo, pero, sin embargo, creo que aún es necesario dar una respuesta correcta a esta pregunta, incluidas las explicaciones sobre por qué y cómo. La mejor respuesta hasta ahora es la que cita exhaustivamente el MSDN: no intente hacer sus propias reglas, los muchachos de MS sabían lo que estaban haciendo.

Pero lo primero es lo primero: la guía como se cita en la pregunta es incorrecta.

Ahora los porqués, hay dos de ellos

Primero por qué : si el código hash se calcula de una manera, que no cambia durante la vida útil de un objeto, incluso si el objeto 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 comparan 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 la creación del objeto, el código hash de objetos iguales debe ser igual". Realmente no sé por qué, pero esa también es la esencia de la mayoría de las respuestas.

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

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

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

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

Segundo por qué : aunque es, por supuesto, cierto, que los cambios en el código hash podrían romper las listas hash y otros objetos que usan el código hash, lo contrario también es cierto. Si no se cambia el código hash, en el peor de los casos se obtendrán listas hash, donde muchos objetos diferentes tendrán el mismo código hash y, por lo tanto, estarán en el mismo bin hash; esto ocurre cuando los objetos se inicializan con un valor estándar, por ejemplo.


Ahora llegando a los cómo Bueno, a primera vista, parece haber una contradicción: de cualquier manera, el código se romperá. Pero ninguno de los problemas proviene de un hashcode modificado o no modificado.

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

De la entrada de la tabla hash de MSDN:

Los objetos clave deben ser inmutables siempre que se utilicen como claves en la tabla hash.

Esto significa:

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

Primero, la forma más fácil, por supuesto, sería diseñar objetos inmutables solo para el uso en tablas hash, que se crearán como copias de los objetos normales y mutables cuando sea necesario. Dentro de los objetos inmutables, obviamente está bien almacenar en caché el código hash, ya que es inmutable.

En segundo lugar, ¿cómo? O bien, asigne al objeto una etiqueta "ahora hash hashed", asegúrese de que todos los datos del objeto sean privados, verifique el indicador en todas las funciones que pueden cambiar los datos de los objetos y arroje un dato de excepción si el cambio no está permitido (es decir, el indicador está configurado ) Ahora, cuando coloque el objeto en cualquier área con hash, asegúrese de establecer la bandera y, también, desarmar la bandera, cuando ya no sea necesaria. Para facilitar su uso, le aconsejo que configure el indicador automáticamente dentro del método "GetHashCode", de esta manera no se puede olvidar. Y la llamada explícita de un método "ResetHashFlag" se asegurará de que el programador tendrá que pensar, ya sea que esté permitido o no cambiar los datos de los objetos por ahora.

Bien, lo que también se debe decir: hay casos en los que es posible tener objetos con datos mutables, sin embargo, el código hash no cambia, cuando los datos de los objetos se cambian, sin violar el contrato de igual y código hash.

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 calcula un valor solo una vez y lo almacena dentro del objeto para devolverlo en llamadas posteriores, entonces debo, nuevamente: absolutamente debe, crear un método Equals, que usará 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 generalmente no lo es.

Por lo tanto, 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 intencional (o simplemente posible), haga que el objeto sea inmutable o cree un indicador de solo lectura para usar para el vida útil de una lista hash que contiene el objeto.

(Por cierto: todo esto no es específico de C # o .NET: está en la naturaleza de todas las implementaciones de tabla hash, o más generalmente de cualquier lista indexada, que los datos de identificación de los objetos nunca deberían cambiar, mientras el objeto está en la lista Se producirá un comportamiento inesperado e impredecible si se rompe esta regla. En algún lugar, puede haber implementaciones de la lista, que supervisan todos los elementos dentro de la lista y reindexan automáticamente la lista, pero el rendimiento de esos seguramente será horrible en el mejor de los casos).

Alex
fuente
23
+1 para esta explicación detallada (daría más si pudiera)
Oliver
55
¡+1 esta es definitivamente la mejor respuesta debido a la explicación detallada! :)
Joe
9

De 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 comparan 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 los valores del objeto cambian, el código hash debería cambiar. Por ejemplo, una clase "Persona" con la propiedad "Nombre" establecida en "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 hubiera querido.


Editar :

También de 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.

De la entrada de la tabla hash de MSDN :

Los objetos clave deben ser inmutables siempre que se utilicen como claves en la tabla hash.

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

En el ejemplo de System.Drawing.Point, el objeto es mutable, y hace devolver un código hash diferente cuando el X o Y los cambios de valor. Esto lo convertiría en un candidato pobre para ser utilizado como está en una tabla hash.

Jon B
fuente
GetHashCode () está diseñado para usarse en una tabla hash, ese es el único punto de esta función.
skolima
@skolima: la documentación de MSDN es inconsistente con eso. Los objetos mutables pueden implementar GetHashCode () y deben devolver valores diferentes a medida que cambia el valor del objeto. Las tablas hash deben usar claves inmutables. Por lo tanto, puede usar GetHashCode () para algo que no sea una tabla hash.
Jon B
9

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 debe ser constante. Por otro lado, MSDN también establece que el valor de retorno de GetHashcode debe ser igual para 2 objetos, si esos 2 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 comparan 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 tiene 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 viola las reglas que se pueden encontrar en MSDN. Supongamos que tiene 2 instancias de esta clase; la propiedad Nombre de instancia1 se establece en 'Pol' y la propiedad Nombre de instancia2 se establece en 'Piet'. Ambas instancias devuelven un código hash diferente, y tampoco son iguales. Ahora, suponga que cambio el Nombre de instancia2 a 'Pol', luego, de acuerdo con mi método Equals, ambas instancias deberían ser iguales y, de acuerdo con una de las reglas de MSDN, deberían devolver el mismo código hash.
Sin embargo, esto no se puede hacer, ya que el código hash de instancia2 cambiará y MSDN indica que esto no está permitido.

Luego, si tiene una entidad, podría implementar el código hash para que use el 'identificador primario' de esa entidad, que tal vez sea una clave sustituta o una propiedad inmutable. Si tiene un objeto de valor, puede implementar el Hashcode para que use las 'propiedades' de ese objeto de valor. Esas propiedades constituyen la 'definición' del objeto de valor. Por supuesto, esta es la naturaleza de un objeto de valor; no te interesa su identidad, sino su valor.
Y, por lo tanto, los objetos de valor deben ser inmutables. (Al igual que están en .NET framework, string, Date, etc. son todos objetos inmutables).

Otra cosa que viene a la mente:
durante la cual 'sesión' (no sé realmente cómo debería llamarlo) debería 'GetHashCode' devolver un valor constante. Suponga que abre su aplicación, carga una instancia de un objeto de la base de datos (una entidad) y obtiene su código hash. Devolverá un cierto número. Cierre la aplicación y cargue la misma entidad. ¿Es necesario 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.

Frederik Gheysels
fuente
1
Su ejemplo es por qué Jeff Yates dice que no puede basar el código hash en los datos mutables. No puede pegar un objeto mutable en un Diccionario y esperar que funcione bien si el código hash se basa en los valores mutables de ese objeto.
Ogre Psalm33
3
¿No puedo ver dónde se viola la regla de MSDN? La regla dice claramente: El método GetHashCode para un objeto debe devolver constantemente 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 . Esto significa que el código hash de instancia2 se puede cambiar cuando cambia el nombre de instancia2 a Pol
chikak el
8

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 durante la vida útil de una instancia. Recuerde que los códigos hash se utilizan para identificar "cubos" en la mayoría de las implementaciones de tablas hash. Si el "cubo" de un objeto cambia, es posible que una tabla hash no pueda encontrar su objeto. Estos pueden ser errores muy difíciles de encontrar, así que hazlo bien la primera vez.

Justin R.
fuente
No lo rechacé, pero supongo que otros lo hicieron porque es una cita que no cubre todo el problema. Las cadenas de simulación eran mutables, pero no cambiaron los códigos hash. Usted crea "bob", lo usa como clave en una tabla hash y luego cambia su valor a "phil". A continuación, cree una nueva cadena "phil". si busca una entrada de tabla hash con la clave "phil", no se encontrará el elemento que introdujo originalmente. Si alguien buscara en "bob", se encontraría, pero obtendría un valor que tal vez ya no sea correcto. Sea diligente para no usar claves que sean mutables o tenga en cuenta los peligros.
Eric Tuttleman
@EricTuttleman: Si estuviera escribiendo las reglas para un marco, habría especificado que para cualquier par de objetos Xy Y, una vez X.Equals(Y)o Y.Equals(X)se haya llamado, todas las llamadas futuras deberían dar el mismo resultado. Si uno quiere usar alguna otra definición de igualdad, use un EqualityComparer<T>.
supercat
5

No responde directamente a su pregunta, pero, si usa Resharper, no olvide que tiene una característica que genera una implementación razonable de GetHashCode (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.

petr k.
fuente
Gracias, en realidad nunca usé Resharper, pero sigo viendo que se menciona con bastante frecuencia, así que debería intentarlo.
Joan Venge el
+1 Resharper si lo tiene genera una buena implementación de GetHashCode.
ΩmegaMan
5

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

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

Y luego echa un vistazo a la publicación de seguimiento (no puedo vincular ya que soy nuevo, pero hay un enlace en el artículo initlal) que analiza más a fondo 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 GetHashCode (), incluso proporciona una descarga de su método junto con algunas otras utilidades, en pocas palabras.

Shaun
fuente
4

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

Si su objeto usa semántica de valores, es decir, la identidad del objeto está definida por sus valores (como Cadena, Color, todas las estructuras). Si la identidad de su objeto es independiente de todos sus valores, entonces el Hashcode se identifica por un subconjunto de sus valores. Por ejemplo, su entrada StackOverflow se almacena en una base de datos en algún lugar. Si cambia su nombre o correo electrónico, su entrada de cliente permanece igual, aunque algunos valores han cambiado (en última instancia, generalmente se identifica con algún número de identificación de cliente largo).

En resumen:

Semántica de tipo de valor: el código hash está definido por valores Semántica de tipo de referencia: el código hash está definido por alguna identificación

Te sugiero que leas Diseño impulsado por dominio de Eric Evans, donde aborda las entidades frente a los tipos de valor (que es más o menos lo que intenté hacer anteriormente) si esto todavía no tiene sentido.

DavidN
fuente
Esto no es realmente correcto. El código hash debe permanecer constante para una instancia particular. En el caso de los tipos de valor, a menudo se da el caso de que cada valor es una instancia única y, por lo tanto, el hash parece cambiar, pero en realidad es una nueva instancia.
Jeff Yates el
Tienes razón, los tipos de valor son inmutables, por lo que impiden el cambio. Buena atrapada.
DavidN