¿Cuál es el mejor algoritmo para anular GetHashCode?
En .NET, el GetHashCode
método se utiliza en muchos lugares de las bibliotecas de clases base de .NET. Implementarlo correctamente es especialmente importante para encontrar elementos rápidamente en una colección o al determinar la igualdad.
¿Existe un algoritmo estándar o una mejor práctica sobre cómo implementarlo GetHashCode
en mis clases personalizadas para no degradar el rendimiento?
Por lo general, opto por algo como la implementación proporcionada en el fabuloso Effective Java de Josh Bloch . Es rápido y crea un hash bastante bueno que es poco probable que provoque colisiones. Elija dos números primos diferentes, por ejemplo, 17 y 23, y haga:
public override int GetHashCode()
{
unchecked // Overflow is fine, just wrap
{
int hash = 17;
// Suitable nullity checks etc, of course :)
hash = hash * 23 + field1.GetHashCode();
hash = hash * 23 + field2.GetHashCode();
hash = hash * 23 + field3.GetHashCode();
return hash;
}
}
Como se señaló en los comentarios, es posible que le resulte mejor elegir un número primo grande para multiplicarlo. Aparentemente 486187739 es bueno... y aunque la mayoría de los ejemplos que he visto con números pequeños tienden a usar números primos, existen al menos algoritmos similares donde a menudo se usan números no primos. En el ejemplo no del todo FNV que aparece más adelante, por ejemplo, he utilizado números que aparentemente funcionan bien, pero el valor inicial no es primo. (Sin embargo, la constante de multiplicación es prima. No sé qué tan importante es eso).
Esto es mejor que la práctica común de XOR
introducir códigos hash por dos razones principales. Supongamos que tenemos un tipo con dos int
campos:
XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y
Por cierto, el algoritmo anterior es el que utiliza actualmente el compilador de C# para tipos anónimos.
Esta página ofrece bastantes opciones. Creo que en la mayoría de los casos lo anterior es "suficientemente bueno" y es increíblemente fácil de recordar y hacer bien. La alternativa FNV es igualmente simple, pero utiliza constantes diferentes y XOR
en lugar de ADD
una operación de combinación. Se parece al código siguiente, pero el algoritmo FNV normal opera en bytes individuales, por lo que sería necesario modificarlo para realizar una iteración por byte, en lugar de por valor hash de 32 bits. FNV también está diseñado para longitudes variables de datos, mientras que la forma en que lo usamos aquí es siempre para la misma cantidad de valores de campo. Los comentarios sobre esta respuesta sugieren que el código aquí en realidad no funciona tan bien (en el caso de muestra probado) como el enfoque de adición anterior.
// Note: Not quite FNV!
public override int GetHashCode()
{
unchecked // Overflow is fine, just wrap
{
int hash = (int) 2166136261;
// Suitable nullity checks etc, of course :)
hash = (hash * 16777619) ^ field1.GetHashCode();
hash = (hash * 16777619) ^ field2.GetHashCode();
hash = (hash * 16777619) ^ field3.GetHashCode();
return hash;
}
}
Tenga en cuenta que una cosa a tener en cuenta es que, idealmente, debería evitar que su estado sensible a la igualdad (y, por lo tanto, sensible al código hash) cambie después de agregarlo a una colección que depende del código hash.
Según la documentación :
Puede anular GetHashCode para tipos de referencia inmutables. En general, para tipos de referencia mutables, debes anular GetHashCode solo si:
- Puede calcular el código hash a partir de campos que no son mutables; o
- Puede asegurarse de que el código hash de un objeto mutable no cambie mientras el objeto esté contenido en una colección que dependa de su código hash.
El enlace al artículo de FNV está roto, pero aquí hay una copia en Internet Archive: Eternally Confuzzled - The Art of Hashing
ValueTuple: actualización para C# 7
Como menciona @cactuaroid en los comentarios, se puede usar una tupla de valor. Esto ahorra algunas pulsaciones de teclas y, lo que es más importante, se ejecuta exclusivamente en la pila (sin basura):
(PropA, PropB, PropC, PropD).GetHashCode();
(Nota: la técnica original que utiliza tipos anónimos parece crear un objeto en el montón, es decir, basura, ya que los tipos anónimos se implementan como clases, aunque el compilador podría optimizar esto. Sería interesante comparar estas opciones, pero la La opción tupla debería ser superior.)
Tipo anónimo (respuesta original)
Microsoft ya proporciona un buen generador genérico de HashCode: simplemente copie los valores de su propiedad/campo a un tipo anónimo y haga un hash:
new { PropA, PropB, PropC, PropD }.GetHashCode();
Esto funcionará para cualquier número de propiedades. No utiliza boxeo. Simplemente utiliza el algoritmo ya implementado en el marco para tipos anónimos.
UsandoSystem.HashCode
Si está utilizando .NET Standard 2.1 o superior, puede utilizar la estructura System.HashCode . En marcos anteriores, está disponible en el Microsoft.Bcl.HashCode
paquete. Hay dos métodos para usarlo:
HashCode.Combinar
El Combine
método se puede utilizar para crear un código hash, con hasta ocho objetos.
public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);
HashCode.Agregar
El Add
método le ayuda a lidiar con colecciones:
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(this.object1);
foreach (var item in this.collection)
{
hashCode.Add(item);
}
return hashCode.ToHashCode();
}
ObtenerHashCode es fácil
Una alternativa a System.HashCode
esto es súper fácil de usar y al mismo tiempo rápida. Puede leer la publicación completa del blog ' GetHashCode Made Easy ' para obtener más detalles y comentarios.
Ejemplo de uso
public class SuperHero
{
public int Age { get; set; }
public string Name { get; set; }
public List<string> Powers { get; set; }
public override int GetHashCode() =>
HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}
Implementación
public struct HashCode : IEquatable<HashCode>
{
private const int EmptyCollectionPrimeNumber = 19;
private readonly int value;
private HashCode(int value) => this.value = value;
public static implicit operator int(HashCode hashCode) => hashCode.value;
public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);
public static bool operator !=(HashCode left, HashCode right) => !(left == right);
public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));
public static HashCode OfEach<T>(IEnumerable<T> items) =>
items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));
public HashCode And<T>(T item) =>
new HashCode(CombineHashCodes(this.value, GetHashCode(item)));
public HashCode AndEach<T>(IEnumerable<T> items)
{
if (items == null)
{
return new HashCode(this.value);
}
return new HashCode(GetHashCode(items, this.value));
}
public bool Equals(HashCode other) => this.value.Equals(other.value);
public override bool Equals(object obj)
{
if (obj is HashCode)
{
return this.Equals((HashCode)obj);
}
return false;
}
public override int GetHashCode() => this.value.GetHashCode();
private static int CombineHashCodes(int h1, int h2)
{
unchecked
{
// Code copied from System.Tuple a good way to combine hashes.
return ((h1 << 5) + h1) ^ h2;
}
}
private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;
private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
{
var temp = startHashCode;
var enumerator = items.GetEnumerator();
if (enumerator.MoveNext())
{
temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
while (enumerator.MoveNext())
{
temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
}
}
else
{
temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
}
return temp;
}
}
¿Qué hace que un buen algoritmo?
Actuación
El algoritmo que calcula un código hash debe ser rápido. Un algoritmo simple suele ser más rápido. Uno que no asigne memoria adicional también reducirá la necesidad de recolección de basura, lo que a su vez también mejorará el rendimiento.
Específicamente en las funciones hash de C#, a menudo se utiliza la unchecked
palabra clave que detiene la comprobación de desbordamiento para mejorar el rendimiento.
determinista
El algoritmo hash debe ser determinista , es decir, dada la misma entrada, siempre debe producir la misma salida.
Reducir colisiones
The algorithm that calculates a hash code needs to keep hash collisions to a minumum. A hash collision is a situation that occurs when two calls to GetHashCode
on two different objects produce identical hash codes. Note that collisions are allowed (some have the misconceptions that they are not) but they should be kept to a minimum.
A lot of hash functions contain magic numbers like 17
or 23
. These are special prime numbers which due to their mathematical properties help to reduce hash collisions as compared to using non-prime numbers.
Hash Uniformity
A good hash function should map the expected inputs as evenly as possible over its output range i.e. it should output a wide range of hashes based on its inputs that are evenly spread. It should have hash uniformity.
Prevent's DoS
In .NET Core each time you restart an application you will get different hash codes. This is a security feature to prevent Denial of Service attacks (DoS). For .NET Framework you should enable this feature by adding the following App.config file:
<?xml version ="1.0"?>
<configuration>
<runtime>
<UseRandomizedStringHashAlgorithm enabled="1" />
</runtime>
</configuration>
Because of this feature, hash codes should never be used outside of the application domain in which they were created, they should never be used as key fields in a collection and they should never be persisted.
Read more about this here.
Cryptographically Secure?
The algorithm does not have to be a Cryptographic hash function. Meaning it does not have to satisfy the following conditions:
- It is infeasible to generate a message that yields a given hash value.
- It is infeasible to find two different messages with the same hash value.
- A small change to a message should change the hash value so extensively that the new hash value appears uncorrelated with the old hash value (avalanche effect).