Redefiniendo GetHashCode

Hola a todos! Un post para comentar paranoias varias sobre algo que parece tan simple como redefinir GetHashCode()…

Primero las dos normas básicas que supongo que la mayoría ya conoceréis:

  1. Si se redefine el método Equals() de una clase debería redefinirse también el método GetHashCode(), para que pueda cumplirse la segunda norma que es…
  2. Si la llamada a Equals para dos objetos devuelve true, entonces GetHashCode() debe devolver el mismo valor para ambos objetos.

Una forma fácil y rápida de implementar GetHashCode() y que cumpla ambas normas es algo así:

public class Foo
{
public int Bar { get; set;}
public int Baz { get; set;}

public override bool Equals(object obj)
{
return obj is Foo && ((Foo)obj).Bar == Bar && ((Foo)obj).Baz == Baz;
}

public override int GetHashCode()
{
return string.Format("{0},{1}", Bar, Baz).GetHashCode();
}
}

Simplemente creamos una representación en cadena del objeto y llamamos a GetHashCode de dicha cadena. ¿Algún problema? Bueno… pues la tercera norma de GetHashCode que no siempre es conocida:

Si a alguien le parece que esta tercera norma entra en contradicción con la segunda, en el caso de objetos mutables… bienvenido al club! 😉

Si no cumplimos esta tercera norma… no podemos objetos de nuestra clase como claves de un diccionario: P.ej. el siguiente test unitario realizado sobre la clase Foo, falla:

[TestClass]
public class FooTests
{
[TestMethod()]
public void FooUsedAsKey()
{
var dict = new Dictionary<Foo, int>();
Foo foo1 = new Foo() { Bar = 10, Baz = 20 };
Foo foo2 = new Foo() { Bar = 10, Baz = 30 };
dict.Add(foo1, 1);
dict.Add(foo2, 2);
foo2.Baz = 20;
int value = dict[foo2];
Assert.AreEqual(2, value); // Assert.AreEqual failed. Expected:<2>. Actual:<1>.
}
}

Esperaríamos que la llamada a dict[foo2] nos devolviese 2, ya que este es el valor asociado con foo2… pero como foo2 ha mutado y ahora devuelve el mismo hashcode que foo1, esa es la entrada que nos devuelve el diccionario… y por eso el Assert falla.

Nota: Si alguien piensa que usando structs en lugar de clases se soluciona el problema… falso: Usando structs ocurre exactamente lo mismo.

Ahora… varias dudas filosóficas:

  1. Alguien entiende que el test unitario está mal? Es decir que el assert debería ser AreEqual(1, value) puesto que si foo2 es igual a foo1, debemos encontrar el valor asociado con foo1, aunque usemos otra referencia (en este caso foo2).
  2. Planteando lo mismo de otro modo: Debemos entender que el diccionario indexa por valor (basándose en equals) o por referencia (basándose en ==)? El caso es entendamos lo que entendamos, la clase Dictionary usa Equals y no ==.
  3. El meollo de todo el asunto ¿Tiene sentido usar objetos mutables como claves en un diccionario?

Yo entiendo que no tiene sentido usar objetos mutables como claves, ya que entonces nos encontramos con todas esas paranoias… y no se vosotros pero yo soy incapaz de escribir un método GetHashCode() para la clase Foo que he expuesto y que se cumpla la tercera condición.

Si aceptamos que usar objetos mutables como claves de un diccionario no tiene sentido, ahora me viene otra pregunta: Dado que es muy normal querer redefinir Equals para objetos mutables, porque se supone que siempre debemos redefinir también GetHashCode? No hubiese sido mucho mejor definir una interfaz IHashable y que el diccionario sólo aceptase claves que implementasen IHashable?

No se… intuyo que la respuesta puede tener que ver con el hecho de que genéricos no aparecieron hasta la versión 2 del lenguaje (en lo que a mi me parece uno de los errores más grandes que Microsoft ha cometido al respecto de .NET), pero quien sabe…

… las mentes de Redmond son inescrutables.

Un saludo!

10 comentarios sobre “Redefiniendo GetHashCode”

  1. Se que han pasado 8 meses desde el post pero como nadie ha comentado nada util te lo pongo por si te vale.

    El framework asume que tenemos campo/s o propiedad/es de instancia de la clase cuyo valor/es es unico para cada instancia que represente un objeto diferente.

    Es mas conceptual que otra cosa. Si las caracteristicas que definen una entidad (atributos de la clase) no pueden identificar unicamente a esa instancia, entonces se necesita un atributo nuevo que lo defina de manera inequivoca. Como los Identificadores de identidad o autonumericos de una base de datos, o los numeros de bastidor de un coche recien salido de fabrica.

    Dependiendo de la arquitectura de la aplicacion se pueden crear Factorias de clases que se encarguen del tabajo de asignar identificadores unicos a las nuevas instacias, constructores copia que tambien pasan ese valor a una copia del objeto o incluso, si tu sistema se lo puede permitir, se puede asumir que si las referencias a los objetos no apuntan a la misma direccion de memoria es que son instancias diferentes que representan elementos diferentes aunque con iguales caracteristicas (estoy casi seguro que esto se puede conseguir en .net a traves de reflexion o marshaling o alguna bizarra de esas); esto seria parecido a asumir que si dos elementos son iguales pero estan a la vez en 2 sitios diferentes pues la logica dicta que son dos objetos diferentes (como en la vida real 😉 ).

    La mayoria de las veces, sobreescribir el getHashCode basicamente consiste en acceder al atributo que identifica unicamente esa instancia (un tipo primitivo) y retornar su getHashCode.

    Resumiendo, necesitas una clave primaria para tus instancias igual que la necesitas para las tuplas en una tabla de BD.

    Un saludo.

  2. Puede que lo que queria decir no lo haya aclarado del todo.

    Ejemplo tontico:

    Mi novia y yo tenemos el mismo coche (1 solo coche que nos pertenece a los dos), en este caso si hicieramos Yo.Coche.Equals(MiNovia.Coche), el resultado deberia ser True.

    Si mi colega Eliphas se compra un coche de mi mismo modelo, color, caracteristicas, etc… la ejecucion de Yo.Coche.Equals(Eliphas.Coche) deberia dar False de resultado.

    Pero, seguro que en mi sistema me encuentro situaciones donde tengo que comparar 2 coches para ver si son iguales (no para ver si son el mismo coche). Yo.Coche.Compare(Eliphas.Coche) que deberia dar True al igual que Yo.Coche.Compare(MiNovia.Coche) tambien daria True.

    Ya que la memoria es como la fisica cuantica, una misma entidad puede estar a la vez en 2 sitios en el espacio/tiempo y aun asi, conceptualmente para nuestro sistema, seguir siendo la misma entidad. Por eso tenemos que disponer en nuestro sistema 3 operaciones de comparacion diferentes.

    La que compara si 2 referencias apuntan al mismo espacio de memoria donde hay una entidad. (operador Is de VB).

    La que compara si 2 entidades son la misma entidad. (Equals) aunque esten en espacios difeerentes. (coch1 Is coche2 podria ser false y aun asi esto podria retornar verdadero)

    La que compara si 2 entidades diferentes son iguales en sus atributos. (Override ==, function Compare, lo que nos inventemos…)

    En los diccionarios de palabras no aparece 2 veces la misma palabra ¿verdad?, pues aqui tampoco aparece 2 veces el mismo coche, aunque pueden aparecer 2 coches con las mismas caracteristicas (el mio y el de Eliphas). Por eso los diccionarios utilizan Equals.

    Espero que se me haya entendido.

  3. @Crowley
    (Espero que tu nick sea en honor al gran Crowley de Buenos Presagios, jejejejee… ;-))

    Entiendo lo que dices, pero creo que tienes un error de concepto:

    >La mayoria de las veces, sobreescribir el getHashCode basicamente consiste en acceder al atributo que identifica unicamente esa instancia

    Pero en ningún sitio se dice que GetHashCode debe ser ÚNICO para cada objeto. Al contrario, lo que la MSDN dice son dos cosas:
    1. Si a.Equals(b) vale true, entonces a.GetHashCode().Equals(b.GetHashCode()) debe valer true también.
    Tiene lógica: dos objetos iguales (aunque no sean el mismo) deben generar el mismo valor de hash.

    2. GetHashCode() devuelve siempre EL MISMO valor durante todo el ciclo de vida del objeto.

    Ambas reglas están en http://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=VS.80).aspx

    Y ahí está el conflicto. Como garantizas que se cumplan ambas reglas A LA VEZ? Desde mi punto de vista sólo hay UNA SOLA MANERA de hacerlo: Con objetos inmutables.

    Pero eso mi pregunta final: Tal y como están pensados los diccionarios en .NET las claves DEBERÍAN SER SIEMPRE OBJETOS INMUTABLES. Pero no hay nada en .NET que «nos fuerce a ello».

    Pero bueno… es «simplemente» otra cosa que echo en falta de .NET: súmala a las referencias constantes, a la noción de método puro y a varias más… 😉

    Un saludo y muchas gracias por tus comentarios!!!

  4. Bueno… Segundo comentario.

    En cuanto escribí este artículo consulté la MSDN, pero parece que en GetHashCode() de la versión 4 del framework han cambiado la descripción. Y no es un cambio baladí.

    En la descripción de GetHashCode para 2.0, pone claramente (http://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=VS.80).aspx)

    .) If two objects of the same type represent the same value, the hash function must return the same constant value for either object.

    .) For the best performance, a hash function must generate a random distribution for all input.

    .) The hash function must return exactly the same value regardless of any changes that are made to the object.

    Por otro lado en la misma MSDN pero consultando para el framework 4, lo que pone es «ligeramente distinto»:

    .) If two objects compare as equal, the GetHashCode method for each object must return the same value. However, if two objects do not compare as equal, the GetHashCode methods for the two object do not have to return different values.

    .) The GetHashCode method for an object must consistently return the same hash code as long as there is no modification to the object state that determines the return value of the object’s Equals method. Note that this is true only for the current execution of an application, and that a different hash code can be returned if the application is run again.

    .) For the best performance, a hash function must generate a random distribution for all input.

    El cambio es notable porque ahora nos dice que GetHashCode puede modificar su valor si se modifica algo en el objeto que hace modificar el valor de Equals.

    Lo que ya no sé es como se comporta el diccionario si eso ocurre. Diria que exactamente igual que antes y que mi test unitario fallaría (no lo he probado). Tiene su lógica, porque en el fondo, todo eso es un problema «conceptual» como bien ha dicho @Crowley.

    Pero es que me encanta filosofar… 😛

  5. >La mayoria de las veces, sobreescribir el getHashCode basicamente consiste en acceder al atributo que identifica unicamente esa instancia

    Me explique mal. Identifica la entidad de forma unica, con independencia de las instancias o punteros que representen o apunten a esa entidad.

    Todo depende de que significado se le de a la relacion entre clave/valor en el diccionario para el sistema.

    Cuando uso un diccionario con un objeto como clave asumo que la relacion es con la entidad unica que representa ese objeto.

    P.E: Si la relacion clave valor se basa en el numero de carreras ganadas por un coche; aunque cambies el color de la pintura, el hash del objeto coche deberia ser el mismo para que se recupere correctamente las carreras ganadas por ese coche. (Esto es lo que yo asumo en mis comentarios anteriores)

    Cuando me encuentro con una situacion en la que la relacion depende de caracteristicas del objeto como modelo de coche y un color (clave) y el numero de gente a la que le gusta esto (valor) se podria hacer un diccionario con una clase llamada Preferencias con modelo de coche y color como atributos y utilizando el GetHashCode() como tu lo redefines el en post, ya que no necesita identificador unico puesto que si cambia algun atributo, esta instancia representa otra preferencia diferente.

    El problema de todo esto de los diccionarios y los hashes de los objetos es que no es independiente del modelado de nuestro sistema y/o arquitectura. Hay que tomar una decision con respecto a que y con que se representa la relacion del par clave/valor.

    Lo que yo he posteado en los comentarios simplemente es la estrategia que yo he seguido y ha sido un caso de exito en mis arquitecturas (hasta el momento, que seguro que ya saldrá el caso expcepcional que me jorobe todo el invento) y he pensado que podria serle util a alguien.

    [OFFTOPIC] PD: El nick viene de Aleister Crowley aka la Bestia. Aunque probablemente el de Buenos Presagios se base en lo mismo puesto que Neil Gaiman ya ha sacado a Aleister Crowley en alguno de sus comics.
    Mi colega Eliphas viene de Eliphas Levi, otro famoso ocultista.

  6. Genial artículo, la verdad es que no tenía muy claro la utilidad y normas de GetHashCode.
    Sólo una pregunta, si tienes la certeza de que tu objeto no va a ser usado como clave en un diccionario ni en una colección de tipo Hash, sigues sobrescribiendo igualmente GetHashCode cuando sobrescribes Equals? Y si lo haces, ahora con 4.0 (según los comentarios del post) parece que ya no hay problema con objetos mutables en que se devuelva un GetHashCode distinto si cambia la PK del objeto (de hecho, lo hará porque la misma PK utilizada para Equals será la que se utilice para GetHashCode), es decir, la tercera regla (la maldita) ya no tiene que cumplirse y no parece un quebradero de cabeza, no?
    Un saludo y gracias por el post!

  7. @Panicoenlaxbox
    Sí, siempre que redefino Equals, redefino GetHashCode y lo hago basandome en todas las propiedades comparadas por Equals (es decir si para dos objetos, Equals devuelve true, GetHashCode devolverá el mismo valor).

    Lo de que este valor sea inmutable durante todo el ciclo de vida del objeto, pues es una norma que NO sigo en el caso de objetos mutables.
    Pero hace tiempo que intento evitar que las claves de mi diccionario sean objetos mutables, la verdad…

    Saludos!

  8. Perfecto, duda aclarada… por cierto, tienes guardado tu blog en un DVD, no? Como un día se caiga Internet y se pierda tu blog, a mí me da algo xD
    Gracias!

Responder a panicoenlaxbox Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *