¿Por qué es importante anular GetHashCode cuando se anula el método Equals?

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;

        if (fooItem == null) 
        {
           return false;
        }

        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?

preguntado el 16 de diciembre de 08 a las 11:12

Es importante implementar tanto equals como gethashcode, debido a colisiones, en particular al usar diccionarios. si dos objetos devuelven el mismo código hash, se insertan en el diccionario con encadenamiento. Mientras se accede al elemento igual se utiliza el método. -

Usando Visual Studio podemos generar Equals () y GetHashCode () basados ​​en los accesorios de nuestra clase. ver este enlace. docs.microsoft.com/en-us/visualstudio/ide/reference/… -

15 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 de dos elementos no coincide, pueden nunca ser considerado igualEquivale simplemente nunca se llamará).

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

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

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

unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
    int hash = 13;
    hash = (hash * 7) + field1.GetHashCode();
    hash = (hash * 7) + field2.GetHashCode();
    ...
    return hash;
}

Oh, para mayor comodidad, también puede considerar proporcionar == y != operadores al anular Equals y GetHashCode.


Una demostración de lo que sucede cuando te equivocas es aquí.

Respondido 24 Abr '20, 02:04

¿Puedo preguntar ahy estás multiplicando con tales factores? - leandro lopez

De hecho, probablemente podría perder a uno de ellos; el punto es tratar de minimizar el número de colisiones, de modo que un objeto {1,0,0} tenga un hash diferente a {0,1,0} y {0,0,1} (si ves lo que quiero decir ), - Marc Gravell ♦

Modifiqué los números para hacerlo más claro (y agregué una semilla). Algunos códigos usan números diferentes; por ejemplo, el compilador de C # (para tipos anónimos) usa una semilla de 0x51ed270b y un factor de -1521134295. - Marc Gravell ♦

@Leandro López: Por lo general, los factores se eligen como números primos porque hace que el número de colisiones sea menor. - andrei rinea

"Oh, por conveniencia, también puede considerar proporcionar == y! = Operadores al anular Equals y GethashCode.": Microsoft desaconseja implementar el operador == para objetos que no son inmutables - msdn.microsoft.com/en-us/library/ms173147.aspx - "No es una buena idea anular el operador == en tipos no inmutables". - antidueño

De hecho, 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 útil de un objeto. Por lo tanto, los campos que se utilizan 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 del ID del objeto. La ID solo se puede establecer a través del constructor, por lo que si desea cambiar la ID, lo cual es muy poco probable, debe crear un nuevo objeto que tenga una nueva ID y, por lo tanto, un nuevo código hash. Este enfoque funciona mejor con GUID porque puede proporcionar un constructor sin parámetros que genera un ID de forma aleatoria.

contestado el 11 de mayo de 10 a las 19:05

@vanja. Creo que tiene que ver con: si agrega el objeto a un diccionario y luego cambia la identificación del objeto, cuando lo busque más tarde, usará un hash diferente para recuperarlo, por lo que nunca lo obtendrá del diccionario. - ANeves piensa que SE es malvado

La documentación de Microsoft de la función GetHashCode () no establece ni implica que el hash del objeto debe permanecer constante durante su vida útil. De hecho, explica específicamente un caso permisible en el que podría no: "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". - PeterAllenWebb

"el código hash no debe cambiar durante la vida útil de un objeto", eso no es cierto. - apocalipsis

Una mejor manera de decirlo es "el código hash (ni la evaluación de iguales) debe cambiar durante el período en que el objeto se usa como clave para una colección". Por lo tanto, si agrega el objeto a un diccionario como clave, debe asegurarse de que GetHashCode y Equals no cambiarán su salida para una entrada determinada hasta que elimine el objeto del diccionario. - scott chambelán

@ScottChamberlain Creo que NO lo olvidó en su comentario, debería ser: "el código hash (ni la evaluación de iguales) NO debe cambiar durante el período en que el objeto se usa como clave para una colección". ¿Derecha? - Stan Prokop

Al anular Equals, básicamente estás indicando que eres el que sabe mejor cómo comparar dos instancias de un tipo determinado, por lo que es probable que seas 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 intenta adivinar un buen código hash basado en todos los campos de la clase, pero como conoce el dominio o los rangos de valores de su objeto, aún podría proporcionar uno mejor.

Respondido 02 Feb 18, 10:02

¿No devolverá esto siempre cero? ¡Probablemente debería inicializar el resultado en 1! También necesita algunos puntos y coma más. - sam mackrill

¿Conoce lo que hace el operador XOR (^)? - Stephen Drew

Como dije, esto es lo que R # escribe para usted (al menos es lo que hizo en 2008) cuando se lo solicitó. Obviamente, este fragmento está destinado a ser modificado por el programador de alguna manera. En cuanto a los puntos y coma que faltan ... sí, parece que los omití cuando copié y pegué el código de una selección de región en Visual Studio. También pensé que la gente lo resolvería ambos. - Trampa

@SamMackrill He agregado los puntos y coma que faltan. - mateo murdoch

@SamMackrill No, no siempre devolverá 0. 0 ^ a = a, asi que 0 ^ m_someVar1 = m_someVar1. Bien podría establecer el valor inicial de result a m_someVar1. - millie smith

No olvide comprobar el parámetro obj con null al anular Equals(). Y también compare el tipo.

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

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

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

Respondido 04 Jul 19, 16:07

Esta verificación de tipo fallará en la situación en la que una subclase se refiere al método Equals de la superclase como parte de su propia comparación (es decir, base.Equals (obj)) - debería usarse como en su lugar - dulcefa

@sweetfa: Depende de cómo se implemente el método Equals de la subclase. También podría llamar a base.Equals ((BaseType) obj)) que funcionaría bien. - jaja

No, no lo hará: msdn.microsoft.com/en-us/library/system.object.gettype.aspx. Y además, la implementación de un método no debería fallar o tener éxito dependiendo de la forma en que se llame. Si el tipo de tiempo de ejecución de un objeto es una subclase de alguna clase base, entonces Equals () de la clase base debería devolver verdadero si obj de hecho es igual a this no importa cómo se haya llamado Equals () de la clase base. - Júpiter

Moviendo el fooItem a la parte superior y luego comprobar si es nulo funcionará mejor en el caso de nulo o un tipo incorrecto. - IS4

@ 40Alpha Bueno, sí, entonces obj as Foo sería inválido. - IS4

Qué tal si:

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

Asumir que el rendimiento no es un problema :)

Respondido 29 ago 17, 09:08

erm - pero está devolviendo una cadena para un método basado en int; _0 - jim tollan

No, sí llama a GetHashCode () desde el objeto String, que devuelve un int. - Ricardo Clayton

No espero que esto sea tan rápido como me gustaría, no solo para el boxeo involucrado para los tipos de valor, sino también para el desempeño de string.Format. Otro friki que he visto es new { prop1, prop2, prop3 }.GetHashCode(). Sin embargo, no puedo comentar cuál sería más lento entre estos dos. No abuse de las herramientas. - nawfal

Esto volverá a ser cierto para { prop1="_X", prop2="Y", prop3="Z" } y { prop1="", prop2="X_Y", prop3="Z_" }. Probablemente no quieras eso. - voetsjoeba

Sí, siempre puede reemplazar el símbolo de subrayado con algo no tan común (por ejemplo, •, ▲, ►, ◄, ☺, ☻) y esperar que sus usuarios no usen estos símbolos ... :) - ludmil tinkov

Tenemos dos problemas que afrontar.

  1. No puedes proporcionar una sensata GetHashCode() si se puede cambiar algún campo en el objeto. Además, a menudo NUNCA se utilizará un objeto en una colección que dependa de GetHashCode(). Entonces, el costo de implementar GetHashCode() a menudo no vale la pena o no es posible.

  2. Si alguien pone tu objeto en una colección que llama GetHashCode() y has anulado Equals() sin hacer tambien 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;

        if (fooItem == null)
        {
           return false;
        }

        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");
    }
}

Respondido 06 Jul 19, 07:07

Lanzar una excepción de GetHashCode es una violación del contrato de Object. No hay dificultad para definir un GetHashCode funcionar de manera que dos objetos cualesquiera que sean iguales devuelvan el mismo código hash; return 24601; y return 8675309; ambas serían implementaciones válidas de GetHashCode. Rendimiento de Dictionary solo será decente cuando la cantidad de elementos sea pequeña, y empeorará mucho si la cantidad de elementos aumenta, pero funcionará correctamente en cualquier caso. - Super gato

@supercat, no es posible implementar GetHashCode de una manera sensata si los campos de identificación en el objeto pueden cambiar, ya que el código hash nunca debe cambiar. Hacer lo que dice podría llevar a alguien a tener que pasar muchos días rastreando el problema de rendimiento, luego muchas semanas en un gran rediseño del sistema para eliminar el uso de los diccionarios. - Ian Ringrose

Solía ​​hacer algo como esto para todas las clases que definí que necesitaban Equals (), y donde estaba completamente seguro de que nunca usaría ese objeto como clave en una colección. Entonces, un día, un programa en el que había usado un objeto como ese como entrada a un control DevExpress XtraGrid se bloqueó. Resulta que XtraGrid, a mis espaldas, estaba creando una HashTable o algo basado en mis objetos. Entré en una pequeña discusión con la gente de soporte de DevExpress sobre esto. Dije que no era inteligente que basaran la funcionalidad y confiabilidad de sus componentes en una implementación de un cliente desconocido de un método oscuro. - RenniePet

La gente de DevExpress fue bastante sarcástica, básicamente diciendo que debo ser un idiota para lanzar una excepción en un método GetHashCode (). Sigo pensando que deberían encontrar un método alternativo para hacer lo que están haciendo; recuerdo a Marc Gravell en un hilo diferente que describe cómo construye un diccionario de objetos arbitrarios sin depender de GetHashCode (); no recuerdo cómo lo hizo aunque. - RenniePet

@RenniePet, es mejor tener un enamoramiento debido a lanzar una excepción, luego tener un error muy difícil de encontrar debido a una implementación no válida. - Ian Ringrose

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

Respondido el 16 de diciembre de 08 a las 20:12

Solo para agregar las respuestas anteriores:

Si no anula Equals, el comportamiento predeterminado es que se comparan las referencias de los objetos. Lo mismo se aplica al código hash: la implementación predeterminada generalmente se basa en una dirección de memoria de la referencia. Debido a que anuló Equals, significa que el comportamiento correcto es comparar lo que implementó 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 el código hash, el objeto igual probablemente tendrá diferentes códigos hash (porque tienen una dirección de memoria diferente) y se determinará incorrectamente como no igual (Equals () ni siquiera se activará).

Además, excepto el problema de que es posible que no pueda encontrar su objeto si lo usó en un diccionario (porque fue insertado por un código hash y cuando lo busque, el código hash predeterminado probablemente será diferente y nuevamente Equals () ni siquiera se llamará, como Marc Gravell explica en su respuesta, también introduce una violación del concepto de diccionario o hashset que no debería permitir claves idénticas; ya declaró que esos objetos son esencialmente los mismos cuando anula Equals, por lo que no No quiero que ambos sean claves diferentes en una estructura de datos que se supone que tienen una clave única, pero debido a que tienen un código hash diferente, la "misma" clave se insertará como una diferente.

Respondido el 07 de Septiembre de 16 a las 11:09

El código hash se utiliza para colecciones basadas en hash como Dictionary, Hashtable, HashSet, etc. El propósito de este código es pre-ordenar rápidamente un objeto específico colocándolo en un grupo específico (depósito). Esta clasificación previa ayuda enormemente a encontrar este objeto cuando necesita recuperarlo de la colecció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 donde 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).

Respondido 21 Feb 12, 11:02

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 donde es posible que no conozca los requisitos de rendimiento. Con frecuencia sé que el tamaño de mi colección no es muy grande y que mi tiempo es más valioso que unos pocos microsegundos de rendimiento obtenidos mediante la creación de un código hash perfecto; entonces (para deshacerme de la molesta advertencia del compilador) simplemente uso:

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

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

Cuando estás en la posición en la que do necesitan 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 la vida útil de un objeto (más exactamente, durante el tiempo en que se necesita el código hash, como cuando se trata de 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 la vida la instancia, por lo que no debe utilizarla 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 el valor 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
      }
   }

Respondido el 27 de junio de 11 a las 00:06

Voto en contra. Esto es completamente incorrecto. Incluso los estados de Microsoft en MSDN (msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx) que el valor de GetHashCode DEBE cambiar cuando el estado del objeto cambia de una manera que pueda afectar el valor de retorno de una llamada a Equals (), e incluso en sus ejemplos también muestra implementaciones de GetHashCode que dependen completamente de valores públicamente cambiables. - Sebastián PR Gingter

Sebastian, no estoy de acuerdo: si agregas un objeto a una colección que usa códigos hash, se colocará en un contenedor que depende del código hash. Si ahora cambia el código hash, no volverá a encontrar el objeto en la colección, ya que se buscará en el contenedor incorrecto. Esto es, de hecho, algo que ha sucedido en nuestro código y por eso me pareció necesario señalarlo. - ILoveFortran

Sebastian, Además, no puedo ver una declaración en el enlace (msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx) que GetHashCode () debe cambiar. Por el contrario, NO debe cambiar siempre que Equals devuelva el mismo valor para el mismo argumento: "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 determina el valor de retorno del método Equals del objeto ". Esta afirmación no implica lo contrario, que debe cambiar si cambia el valor de retorno de Equals. - ILoveFortran

@Joao, está confundiendo el lado del cliente / consumidor del contrato con el productor / implementador. Me refiero a la responsabilidad del implementador, que anula GetHashCode (). Estás hablando del consumidor, el que está usando el valor. - ILoveFortran

Completo malentendido ... :) La verdad es que el código hash debe cambiar cuando cambia el estado del objeto, a menos que el estado sea irrelevante para la identidad del objeto. Además, nunca debe usar un objeto MUTABLE como clave en sus colecciones. Utilice objetos de solo lectura para este propósito. GetHashCode, Equals ... y algunos otros métodos cuyos nombres no recuerdo en este momento NUNCA deberían arrojar. - cariño

A partir del .NET 4.7 el método preferido de anulación GetHashCode() se muestra a continuación. Si se dirige a versiones anteriores de .NET, incluya el Nuget System.ValueTuple paquete.

// C# 7.0+
public override int GetHashCode() => (FooId, FooName).GetHashCode();

En términos de rendimiento, este método superará a la mayoría compuesto implementaciones de código hash. La Tupla de valor es un struct por lo que no habrá basura, y el algoritmo subyacente es tan rápido como es posible.

contestado el 11 de mayo de 20 a las 13:05

Siempre debe garantizar que si dos objetos son iguales, según lo definido por Equals (), deben devolver el mismo código hash. Como afirman algunos de los otros comentarios, en teoría esto no es obligatorio si el objeto nunca se utilizará en un contenedor basado en hash como HashSet o Dictionary. Sin embargo, te aconsejo que sigas siempre esta regla. La razón es simplemente porque es demasiado fácil para alguien cambiar una colección de un tipo a otro con la buena intención de mejorar realmente el rendimiento o simplemente transmitir la semántica del código de una mejor manera.

Por ejemplo, supongamos que mantenemos algunos objetos en una lista. Algún tiempo después, alguien se da cuenta de que un HashSet es una alternativa mucho mejor debido a las mejores características de búsqueda, por ejemplo. Aquí es cuando podemos meternos en problemas. List usaría internamente el comparador de igualdad predeterminado para el tipo que significa Igual en su caso, mientras que HashSet hace uso de GetHashCode (). Si los dos se comportan de manera diferente, también lo hará su programa. Y tenga en cuenta que estos problemas no son los más fáciles de solucionar.

He resumido este comportamiento con algunos otros errores de GetHashCode () en un blog donde puede encontrar más ejemplos y explicaciones.

Respondido 12 Feb 20, 20:02

A partir del C # 9(.net 5 o .net core 3.1), es posible que desee utilizar archivos como lo hace Igualdad basada en valores.

Respondido el 15 de enero de 21 a las 11:01

Tengo entendido que el GetHashCode () original devuelve la dirección de memoria del objeto, por lo que es esencial anularlo si desea comparar dos objetos diferentes.

EDITADO: Eso fue incorrecto, el método GetHashCode () original no puede asegurar la igualdad de 2 valores. Aunque los objetos que son iguales devuelven el mismo código hash.

Respondido el 09 de diciembre de 13 a las 19:12

A continuación, usar la reflexión me parece una mejor opción considerando las propiedades públicas, ya que con esto no tiene que preocuparse por la adición / eliminación de propiedades (aunque no es un escenario tan común). También encontré que esto funciona mejor (comparado con el tiempo usando el cronómetro de Diagonistics).

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }

respondido 25 mar '14, 18:03

Se espera que la implementación de GetHashCode () sea muy ligera. No estoy seguro de que el uso de la reflexión se note con StopWatch en miles de llamadas, pero seguramente en millones (piense en completar un diccionario de una lista). - bohdan_trotsenko

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.