viernes, 21 de mayo de 2010

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!



PD: Para variar esto es un crosspost desde mi blog en geeks.ms! Pásate mejor por allí que somos más!

No hay comentarios: