studio - gethashcode c# override
¿Por qué es importante anular GetHashCode cuando se invalida el método Equals? (12)
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 método Equals
porque Foo
representa una fila para la tabla de Foo
. ¿Cuál es el método preferido para anular el GetHashCode
?
¿Por qué es importante anular GetHashCode
?
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 agregar / eliminar propiedades (aunque no es un escenario tan común). También encontré que este rendimiento es mejor (el tiempo comparado con 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;
}
Al anular Equals, básicamente estás diciendo que eres el que sabe mejor cómo comparar dos instancias de un tipo dado, 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 trata de adivinar un buen código hash basado en todos los campos de la clase, pero como sabe el dominio de su objeto o los rangos de valores, aún podría proporcionar uno mejor.
El código hash se usa para colecciones basadas en hash como Dictionary, Hashtable, HashSet, etc. El propósito de este código es ordenar rápidamente un objeto específico poniéndolo en un grupo específico (cubo). Esta preclasificación ayuda enormemente 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 cubo 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).
En realidad, es muy difícil implementar GetHashCode()
correctamente porque, además de las reglas que Marc ya mencionó, el código hash no debe 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 trabajaba con NHibernate. Mi enfoque es calcular el código hash a partir del ID del objeto. El ID solo se puede establecer a través del constructor, por lo que si desea cambiar el ID, que 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 una ID.
Esto se debe a que el marco requiere que dos objetos que sean iguales tengan el mismo código hash. Si reemplaza el método equals para hacer una comparación especial de dos objetos y los dos objetos son considerados iguales por el método, entonces el código hash de los dos objetos también debe ser el mismo. (Los diccionarios y Hashtables se basan en este principio).
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 es posible que no conozca los requisitos de rendimiento. Con frecuencia sé que los tamaños de mis colecciones no son muy grandes y mi tiempo es más valioso que unos pocos microsegundos de rendimiento que se obtienen al crear un código hash perfecto; así que (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 manera.)
Por supuesto, cuando está en la posición de que necesita el rendimiento, todos los problemas mencionados por otros aquí se aplican. Lo más importante es que , de lo contrario, obtendrá resultados erróneos al recuperar elementos de un hash o diccionario: el código hash no debe variar con la vida útil de un objeto (más precisamente, durante el tiempo que se necesita el código hash, como cuando una clave en un diccionario): por ejemplo, lo siguiente es incorrecto, ya que el Valor es público y, por lo tanto, puede cambiarse externamente a la clase durante el tiempo de vida de la instancia, por lo que no debe usarlo 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 no se puede cambiar el valor, 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
}
}
No olvide verificar el parámetro obj con el null
al anular Equals()
. Y también comparar 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;
}
El motivo de esto es: Equals
debe devolver false en comparación con null
. Consulte también http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx
Qué tal si:
public override int GetHashCode()
{
return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}
Suponiendo que el rendimiento no es un problema :)
Sí, es importante si su artículo se usará como clave en un diccionario, o HashSet<T>
, etc., ya que se usa (en ausencia de un IEqualityComparer<T>
) para agrupar los elementos en grupos. Si el código hash para dos elementos no coincide, puede que nunca se consideren iguales (simplemente no se llamará a los Equals
).
El método GetHashCode()
debe reflejar la lógica Equals
; las reglas son:
- si dos cosas son iguales (
Equals(...) == true
), entonces deben devolver el mismo valor paraGetHashCode()
- si el
GetHashCode()
es igual, no es necesario que sean iguales; esto es una colisión, y se llamará aEquals
para ver si es una igualdad real o no.
En este caso, parece que " return FooId;
" es una implementación adecuada de GetHashCode()
. Si está probando múltiples propiedades, es común combinarlas utilizando el código como el siguiente, para reducir las colisiones diagonales (es decir, para que el new Foo(3,5)
tenga un código hash diferente al new Foo(5,3)
):
int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;
Oh, por conveniencia, también podría considerar proporcionar operadores ==
y !=
Al anular Equals
y GetHashCode
.
Una demostración de lo que sucede cuando te equivocas está here .
Sólo para agregar en las respuestas anteriores:
Si no reemplaza a Equals, entonces 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. Como anuló Igals, significa que el comportamiento correcto es comparar lo que implementó en Equals y no las referencias, por lo que debe hacer lo mismo para 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 caro 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á erróneamente como no igual (Igual () no llegará).
Además, excepto por el problema, 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 el mismo) ni siquiera se llamará, como explica Marc Gravell en su respuesta, también introduces una violación del concepto de diccionario o hashset que no debería permitir claves idénticas: ya declaraste que esos objetos son esencialmente iguales cuando anulas Equals por lo que no No desee que ambas sean claves diferentes en una estructura de datos que suponga tener una clave única, pero como tienen un código hash diferente, la clave "igual" se insertará como una clave diferente.
Tenemos dos problemas para hacer frente.
No puede proporcionar un
GetHashCode()
sensible si se puede cambiar cualquier campo en el objeto. También a menudo un objeto NUNCA se utilizará en una colección que depende deGetHashCode()
. Entonces, el costo de implementarGetHashCode()
menudo no vale la pena, o no es posible.Si alguien coloca su objeto en una colección que llama a
GetHashCode()
y usted ha anuladoEquals()
sin hacer queGetHashCode()
comporte 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");
}
}
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 original GetHashCode () no puede asegurar la igualdad de 2 valores. Aunque los objetos que son iguales devuelven el mismo código hash.