Como no pude encontrar una respuesta que explique por qué deberíamos anular GetHashCode
y Equals
para estructuras personalizadas y por qué la implementación predeterminada "no es probable que sea adecuada para usar como clave en una tabla hash", dejaré un enlace a este blog post , que explica por qué con un ejemplo de caso real de un problema que sucedió.
Recomiendo leer la publicación completa, pero aquí hay un resumen (énfasis y aclaraciones añadidas).
Razón por la cual el hash predeterminado para estructuras es lento y no muy bueno:
La forma en que está diseñado el CLR, cada llamada a un miembro definido en System.ValueType
o System.Enum
tipos [puede] causar una asignación de boxeo [...]
Un implementador de una función hash se enfrenta a un dilema: hacer una buena distribución de la función hash o hacerla rápida. En algunos casos, es posible alcanzar a los dos, pero es difícil hacer esto de forma genérica en ValueType.GetHashCode
.
La función hash canónica de una estructura "combina" códigos hash de todos los campos. Pero la única forma de obtener un código hash de un campo en un ValueType
método es usar la reflexión . Por lo tanto, los autores de CLR decidieron cambiar la velocidad sobre la distribución y la GetHashCode
versión predeterminada solo devuelve un código hash de un primer campo no nulo y lo "combina" con una identificación de tipo [...] Este es un comportamiento razonable a menos que no sea . Por ejemplo, si tiene la mala suerte y el primer campo de su estructura tiene el mismo valor para la mayoría de las instancias, entonces una función hash proporcionará el mismo resultado todo el tiempo. Y, como puede imaginar, esto causará un impacto drástico en el rendimiento si estas instancias se almacenan en un conjunto hash o una tabla hash.
[...] La implementación basada en la reflexión es lenta . Muy lento.
[...] Ambos ValueType.Equals
y ValueType.GetHashCode
tienen una optimización especial. Si un tipo no tiene "punteros" y está empaquetado [...] correctamente, se utilizan versiones más óptimas: GetHashCode
itera sobre una instancia y bloques XOR de 4 bytes y el Equals
método compara dos instancias usando memcmp
. [...] Pero la optimización es muy complicada. Primero, es difícil saber cuándo se habilita la optimización [...] Segundo, una comparación de memoria no necesariamente le dará los resultados correctos . Aquí hay un ejemplo simple: [...] -0.0
y +0.0
son iguales pero tienen diferentes representaciones binarias.
Problema del mundo real descrito en la publicación:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Utilizamos una tupla que contenía una estructura personalizada con implementación de igualdad predeterminada. Y desafortunadamente, la estructura tenía un primer campo opcional que casi siempre era igual a [cadena vacía] . El rendimiento estuvo bien hasta que el número de elementos en el conjunto aumentó significativamente causando un problema de rendimiento real, tomando minutos para inicializar una colección con decenas de miles de elementos.
Por lo tanto, para responder la pregunta "en qué casos debo empacar el mío y en qué casos puedo confiar con seguridad en la implementación predeterminada", al menos en el caso de las estructuras , debe anular Equals
y GetHashCode
cada vez que su estructura personalizada se pueda usar como clave en una tabla hash o Dictionary
.
También recomendaría implementar IEquatable<T>
en este caso, para evitar el boxeo.
Como dicen las otras respuestas, si está escribiendo una clase , el hash predeterminado que usa la igualdad de referencia generalmente está bien, por lo que no me molestaría en este caso, a menos que necesite anular Equals
(entonces tendría que anular en GetHashCode
consecuencia).