Sobreescribiendo GetHashCode para objetos mutables?

He leído aproximadamente 10 preguntas diferentes sobre cuándo y cómo anular GetHashCode pero todavía hay algo que no entiendo del todo. La mayoría de las implementaciones de GetHashCode se basan en los códigos hash de los campos del objeto, pero se ha mencionado que el valor de GetHashCode nunca debería cambiar durante la vigencia del objeto. ¿Cómo funciona eso si los campos en los que está basado son mutables? Además, ¿qué sucede si quiero que las búsquedas de diccionario, etc., se basen en la igualdad de referencia, no en mi Equals invalidado?

Estoy anulando principalmente Equals por la facilidad de la unidad de prueba de mi código de serialización, que asumo serializar y deserializar (a XML en mi caso) mata la igualdad de referencia, así que quiero asegurarme de que al menos sea correcta en igualdad de valores. ¿Es esta una mala práctica anular Equals en este caso? Básicamente, en la mayoría de los códigos de ejecución, deseo igualdad de referencia y siempre uso == y no estoy anulando eso. ¿Debo simplemente crear un nuevo método ValueEquals o algo en lugar de anular Equals ? Solía ​​suponer que el marco siempre usa == y no Equals a comparar cosas, por lo que pensé que era seguro anular Equals ya que me pareció que su propósito era si quieres tener una segunda definición de igualdad que es diferente de el operador == . Después de leer muchas otras preguntas, parece que ese no es el caso.

EDITAR:

Parece que mis intenciones no estaban claras, lo que quiero decir es que el 99% del tiempo quiero una igualdad de referencia normal, comportamiento predeterminado, sin sorpresas. Para casos muy raros, deseo tener valor de igualdad, y quiero solicitar explícitamente igualdad de valor usando .Equals lugar de == .

Cuando hago esto, el comstackdor recomienda que anule también GetHashCode , y así surgió esta pregunta. Parecía que hay objectives contradictorios para GetHashCode cuando se aplica a objetos mutables, que son:

  1. Si a.Equals(b) entonces a.GetHashCode() debería == b.GetHashCode() .
  2. El valor de a.GetHashCode() nunca debe cambiar durante el tiempo de vida de a .

Estos parecen naturalmente contradictorios cuando un objeto mutable, porque si el estado del objeto cambia, esperamos que el valor de .Equals() cambie, lo que significa que GetHashCode debería cambiar para coincidir con el cambio en .Equals() , pero GetHashCode no debería cambio.

¿Por qué parece haber esta contradicción? ¿Estas recomendaciones no están destinadas a aplicarse a objetos mutables? Probablemente se supone, pero podría valer la pena mencionar que me refiero a las clases no a las estructuras.

Resolución:

Estoy marcando a JaredPar como aceptado, pero principalmente para la interacción de comentarios. Para resumir lo que he aprendido de esto es que la única manera de lograr todos los objectives y evitar posibles comportamientos extravagantes en casos IEquatable es solo anular Equals y GetHashCode basados ​​en campos inmutables, o implementar IEquatable . Este tipo de parece disminuir la utilidad de reemplazar Equals para los tipos de referencia, ya que de lo que he visto la mayoría de los tipos de referencia no tienen campos inmutables a menos que estén almacenados en una base de datos relacional para identificarlos con sus claves principales.

¿Cómo funciona eso si los campos en los que está basado son mutables?

No lo hace en el sentido de que el código hash cambiará a medida que el objeto cambie. Ese es un problema por todos los motivos enumerados en los artículos que lee. Lamentablemente, este es el tipo de problema que, por lo general, solo aparece en casos de esquina. Entonces los desarrolladores tienden a salirse con la mala conducta.

Además, ¿qué sucede si quiero que las búsquedas de diccionario, etc., se basen en la igualdad de referencia, no en mi Equivalente invalidado?

Siempre que implemente una interfaz como IEquatable esto no debería ser un problema. La mayoría de las implementaciones de diccionarios elegirán un comparador de igualdad de una manera que use IEquatable sobre Object.ReferenceEquals. Incluso sin IEquatable , la mayoría utilizará Object.Equals () de manera predeterminada, que luego se aplicará a su implementación.

Básicamente, en la mayoría de los códigos de ejecución, deseo igualdad de referencia y siempre uso == y no estoy anulando eso.

Si espera que sus objetos se comporten con igualdad de valores, debe anular == y! = Para aplicar la igualdad de valores para todas las comparaciones. Los usuarios aún pueden usar Object.ReferenceEquals si realmente quieren igualdad de referencia.

Solía ​​suponer que el marco siempre usa == y no equivale a comparar cosas

Lo que BCL usa ha cambiado un poco con el tiempo. Ahora, la mayoría de los casos que usan igualdad tomarán una IEqualityComparer y la usarán para la igualdad. En los casos en que no se especifique uno, utilizarán EqualityComparer.Default para encontrar uno. En el peor de los casos, esto dará lugar a la invocación de Object.Equals

Si tiene un objeto mutable, no tiene mucho sentido anular el método GetHashCode, ya que no puede usarlo realmente. Se usa, por ejemplo, en las colecciones Dictionary y HashSet para colocar cada elemento en un cubo. Si cambia el objeto mientras se usa como clave en la colección, el código hash ya no coincide con el depósito en el que se encuentra el objeto, por lo que la colección no funciona correctamente y es posible que nunca vuelva a encontrar el objeto.

Si desea que la búsqueda no use el método GetHashCode o Equals de la clase, siempre puede proporcionar su propia implementación de IEqualityComparer para utilizar en su lugar al crear el Dictionary .

El método Equals está destinado a la igualdad de valores, por lo que no está mal implementarlo de esa manera.

Wow, eso es en realidad varias preguntas en una :-). Entonces uno después del otro:

se ha mencionado que el valor de GetHashCode nunca debería cambiar durante la vigencia del objeto. ¿Cómo funciona eso si los campos en los que está basado son mutables?

Este consejo común está destinado para el caso en el que desee utilizar su objeto como clave en una HashTable / diccionario, etc. Los HashTables generalmente requieren que el hash no cambie, ya que lo usan para decidir cómo almacenar y recuperar la clave. Si el hash cambia, es probable que la HashTable ya no encuentre su objeto.

Para citar los documentos de la interfaz de Mapa de Java:

Nota: se debe tener mucho cuidado si los objetos mutables se utilizan como claves de mapa. El comportamiento de un mapa no se especifica si el valor de un objeto se cambia de una manera que afecta a las comparaciones iguales mientras el objeto es una clave en el mapa.

En general, es una mala idea usar cualquier tipo de objeto mutable como clave en una tabla hash: ni siquiera está claro qué sucedería si una clave cambia después de que se ha agregado a la tabla hash. ¿Debería la tabla hash devolver el objeto almacenado a través de la clave anterior, o mediante la nueva clave, o mediante ambos?

Entonces, el verdadero consejo es: solo use objetos inmutables como claves, y asegúrese de que su código hash nunca cambie (lo cual es generalmente automático si el objeto es inmutable).

Además, ¿qué sucede si quiero que las búsquedas de diccionario, etc., se basen en la igualdad de referencia, no en mi Equivalente invalidado?

Bueno, encuentra una implementación de diccionario que funcione así. Pero los diccionarios estándar de la biblioteca usan el hashcode & Equals, y no hay forma de cambiar eso.

Estoy anulando principalmente Equals por la facilidad de la unidad de prueba de mi código de serialización, que asumo serializar y deserializar (a XML en mi caso) mata la igualdad de referencia, así que quiero asegurarme de que al menos sea correcta en igualdad de valores. ¿Es esta una mala práctica anular Equals en este caso?

No, encontraría eso perfectamente aceptable. Sin embargo, no debe usar objetos como claves en un diccionario / hashtable, ya que son mutables. Véase más arriba.

El tema subyacente aquí es cómo identificar mejor los objetos de manera única. Menciona la serialización / deserialización que es importante porque la integridad referencial se pierde en ese proceso.

La respuesta corta, es que los objetos deben identificarse de manera única por el conjunto más pequeño de campos inmutables que se pueden usar para hacerlo. Estos son los campos que debe usar al anular GetHashCode e Igual.

Para las pruebas, es perfectamente razonable definir las aserciones que necesite, generalmente estas no están definidas en el tipo en sí, sino como métodos de utilidad en el conjunto de pruebas. Tal vez un TestSuite.AssertEquals (MyClass, MyClass)?

Tenga en cuenta que GetHashCode e Igual deben funcionar juntos. GetHashCode debería devolver el mismo valor para dos objetos si son iguales. Equals debería devolver verdadero si y solo si dos objetos tienen el mismo código hash. (Tenga en cuenta que es posible que dos objetos no sean iguales, pero pueden devolver el mismo código hash). Hay un montón de páginas web que abordan este tema directamente, solo en google.

No sé acerca de C #, siendo un novato relativo a ella, pero en Java, si anulas equals () también debes anular hashCode () para mantener el contrato entre ellos (y viceversa) … Y java también tiene la misma captura 22; básicamente forzándolo a usar campos inmutables … Pero este es un problema solo para las clases que se usan como hash-key, y Java tiene implementaciones alternativas para todas las colecciones basadas en hash … que tal vez no tan rápido, pero lo hacen de manera efectiva Permitirle usar un objeto mutable como clave … simplemente (generalmente) está mal visto como un “diseño pobre”.

Y siento la necesidad de señalar que este problema fundamental es intemporal … Ha existido desde que Adam era un muchacho.

He trabajado en el código Fortran, que es más antiguo que yo (tengo 36) y que se rompe cuando se cambia un nombre de usuario (como cuando una chica se casa o se divorcia 😉 … Así es la ingeniería, la solución adoptada fue : El “método” GetHashCode recuerda el hashCode calculado previamente, recalcula el hashCode (es decir, un marcador virtual es Dirty) y si los campos clave han cambiado, devuelve null. Esto hace que la memoria caché elimine al usuario “sucio” (llamando a otro GetPreviousHashCode) y luego la memoria caché devuelve un valor nulo, haciendo que el usuario vuelva a leer de la base de datos. Un truco interesante y que vale la pena; incluso si lo digo yo mismo 😉

Cambiaré la mutabilidad (solo deseable en casos de esquina) para O (1) acceso (deseable en todos los casos). Bienvenido a la ingeniería; la tierra del compromiso informado.

Aclamaciones. Keith.