Pregunta ¿Por qué es importante anular GetHashCode cuando el método Equals es anulado?


Dada la siguiente clase

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

He anulado el Equals método porque Foo representar una fila para el Foos mesa. ¿Cuál es el método preferido para anular el GetHashCode?

Por qué es importante anular GetHashCode?


1164
2017-12-16 13:41


origen


Respuestas:


Sí, es importante si su artículo se utilizará como clave en un diccionario, o HashSet<T>, etc., ya que esto se usa (en ausencia de una costumbre IEqualityComparer<T>) para agrupar elementos en cubos. Si el código hash para dos elementos no coincide, pueden Nunca ser considerado igual (Equals simplemente nunca se llamará).

los GetHashCode() método debe reflejar el Equals lógica; las reglas son:

  • si dos cosas son iguales (Equals(...) == true) entonces ellos debe devolver el mismo valor para GetHashCode()
  • Si el GetHashCode() es igual, es no necesario para que ellos sean lo mismo; esto es una colisión, y Equals se llamará para ver si es una igualdad real o no.

En este caso, parece que "return FooId;"es un adecuado GetHashCode() implementación. Si está probando varias propiedades, es común combinarlas usando el código siguiente, para reducir las colisiones diagonales (es decir, new Foo(3,5) tiene un código hash diferente para new Foo(5,3))

int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;

Ah, para su comodidad, también podría considerar proporcionar == y != operadores al anular Equals y GetHashCode.


Una demostración de lo que sucede cuando obtienes este error es aquí.


1097
2017-12-16 13:47



En realidad es muy difícil de implementar GetHashCode() correctamente porque, además de las reglas que Marc ya mencionó, el código hash no debería cambiar durante la vida de un objeto. Por lo tanto, los campos que se usan para calcular el código hash deben ser inmutables.

Finalmente encontré una solución a este problema cuando estaba trabajando con NHibernate. Mi enfoque es calcular el código hash a partir de la ID del objeto. El ID solo se puede establecer a través del constructor, por lo que si desea cambiar el ID, lo cual es muy poco probable, debe crear un nuevo objeto que tenga un nuevo ID y, por lo tanto, un nuevo código hash. Este enfoque funciona mejor con los GUID porque puede proporcionar un constructor sin parámetros que genere aleatoriamente un ID.


114
2017-12-21 12:39



Al anular Equals básicamente está diciendo que es usted el que mejor sabe cómo comparar dos instancias de un tipo determinado, por lo que es probable que sea el mejor candidato para proporcionar el mejor código hash.

Este es un ejemplo de cómo ReSharper escribe una función GetHashCode () para usted:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

Como puede ver, solo trata de adivinar un buen código hash basado en todos los campos de la clase, pero dado que conoce el dominio o los rangos de valores de su objeto, podría proporcionar uno mejor.


41
2017-12-16 13:48



Por favor, no olvides verificar el parámetro obj contra null cuando prevalece Equals(). Y también compara el tipo.

public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

La razón de esto es: Equals debe devolver falso en comparación con null. Ver también http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx


32
2017-11-17 07:46



Qué tal si:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

Asumir el rendimiento no es un problema :)


23
2017-11-25 00:48



Es porque el marco requiere que dos objetos que son iguales deben tener el mismo código hash. Si anula el método igual para hacer una comparación especial de dos objetos y el objeto considera los dos objetos iguales, entonces el código hash de los dos objetos también debe ser el mismo. (Dictionaries y Hashtables se basan en este principio).


9
2017-12-16 13:48



Solo para agregar las respuestas anteriores:

Si no anula Equals, entonces el comportamiento predeterminado es comparar las referencias de los objetos. Lo mismo se aplica a hashcode: la implementación predeterminada generalmente se basa en una dirección de memoria de la referencia. Como anuló Igual significa que el comportamiento correcto es comparar lo que implemente en Equals y no las referencias, por lo que debe hacer lo mismo con el código hash.

Los clientes de su clase esperarán que el código hash tenga una lógica similar al método equals, por ejemplo, los métodos linq que usan un IEqualityComparer primero comparan los códigos hash y solo si son iguales compararán el método Equals () que podría ser más costoso. para ejecutar, si no implementamos código hash, el objeto igual probablemente tendrá códigos hash diferentes (porque tienen una dirección de memoria diferente) y se determinará erróneamente como no igual (Equals () ni siquiera golpeará).

Además, excepto el problema de que es posible que no puedas encontrar tu objeto si lo usaste en un diccionario (porque fue insertado por un código hash y cuando lo buscas, el código hash predeterminado probablemente será diferente y nuevamente el igual () ni siquiera se llamará, como explica Marc Gravell en su respuesta, también introduce una violación del concepto de diccionario o hashset que no debería permitir teclas idénticas - usted ya ha declarado que esos objetos son esencialmente iguales cuando reemplaza Equals, por lo que no los quiere a ambos como claves diferentes en una estructura de datos que suponga tener una clave única. Pero debido a que tienen un código hash diferente, la "misma" clave se insertará como una diferente.


8
2017-11-12 13:48



Tenemos dos problemas con los que lidiar.

  1. No puedes proporcionar una sensata GetHashCode() si cualquier campo en el objeto puede ser cambiado. También a menudo un objeto NUNCA se usará en un colección que depende de GetHashCode(). Entonces el costo de implementar GetHashCode() a menudo no vale la pena, o no es posible.

  2. Si alguien pone su objeto en una colección que llama GetHashCode() y has anulado Equals() sin hacer también GetHashCode() comportarse de manera correcta, esa persona puede pasar días rastreando el problema

Por lo tanto, por defecto lo hago.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}

7
2017-11-19 10:17



El código Hash se utiliza para colecciones basadas en hash como Dictionary, Hashtable, HashSet, etc. El objetivo de este código es predeterminar rápidamente un objeto específico poniéndolo en un grupo específico (bucket). Esta ordenación previa ayuda tremendamente a encontrar este objeto cuando necesita recuperarlo de la recopilación de hash porque el código tiene que buscar su objeto en un solo depósito en lugar de en todos los objetos que contiene. La mejor distribución de los códigos hash (mejor singularidad) la recuperación más rápida. En una situación ideal en la que cada objeto tiene un código hash único, encontrarlo es una operación O (1). En la mayoría de los casos se acerca a O (1).


5
2018-02-21 11:36



No es necesariamente importante; depende del tamaño de sus colecciones y sus requisitos de rendimiento, y de si su clase se utilizará en una biblioteca en la que quizás no conozca los requisitos de rendimiento. Con frecuencia sé que los tamaños de mi colección no son muy grandes y mi tiempo es más valioso que unos pocos microsegundos de rendimiento obtenidos al crear un código hash perfecto; así que (para deshacerse de la molesta advertencia del compilador) simplemente uso:

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(Por supuesto que podría usar un #pragma para desactivar la advertencia también, pero prefiero de esta manera).

Cuando estás en la posición que tienes hacer Necesita el rendimiento que todos los problemas mencionados por otros aquí se aplican, por supuesto. Lo más importante - De lo contrario, obtendrá resultados incorrectos al recuperar elementos de un conjunto de hash o diccionario: el código hash no debe variar con el tiempo de vida de un objeto (más exactamente, durante el tiempo siempre que se necesite el código hash, como ser una clave en un diccionario): por ejemplo, lo siguiente es incorrecto ya que Value es público y, por lo tanto, se puede cambiar externamente a la clase durante el tiempo de vida de la instancia, por lo que no debe usarla como base para el código hash:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

Por otro lado, si Value no se puede cambiar, está bien usar:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }


3
2018-06-26 23:21