¿Cuál es el mejor algoritmo para anular GetHashCode?

1449

En .NET, el GetHashCodemétodo se usa en muchos lugares de las bibliotecas de clases base .NET. Implementarlo adecuadamente 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 implementar GetHashCodemis clases personalizadas para no degradar el rendimiento?

bitbonk
fuente
38
Después de leer esta pregunta y el artículo a continuación, podría implementar la anulación de GetHashCode. Espero que sea útil para otros. Pautas y reglas para GetHashCode escrito por Eric Lippert
rene
44
"o para determinar la igualdad": ¡no! Dos objetos con el mismo código hash no son necesariamente iguales.
Thomas Levesque
1
@ThomasLevesque Tienes razón, dos objetos con el mismo código hash no son necesariamente iguales. Pero todavía GetHashCode()se usa en muchas implementaciones de Equals(). Eso es lo que quise decir con esa declaración. GetHashCode()adentro a Equals()menudo se usa como un atajo para determinar la desigualdad , porque si dos objetos tienen un código hash diferente , deben ser objetos que no son iguales y el resto de la verificación de igualdad no tiene que ejecutarse.
bitbonk
3
@bitbonk Por lo general, ambos GetHashCode()y Equals()necesitan mirar todos los campos de ambos objetos (Equals tiene que hacer esto si los códigos hash son iguales o no están marcados). Debido a esto, una llamada al GetHashCode()interior a Equals()menudo es redundante y podría reducir el rendimiento. Equals()también puede ser capaz de provocar un cortocircuito, lo que lo hace mucho más rápido; sin embargo, en algunos casos, los códigos hash pueden almacenarse en caché, lo que hace que la GetHashCode()verificación sea más rápida y valga la pena. Vea esta pregunta para más.
NotEnoughData
ACTUALIZACIÓN ENERO 2020: Blog de Eric Lippert ubicado en: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

Respuestas:

1604

Por lo general, uso algo como la implementación dada en el fabuloso Java eficaz de Josh Bloch . Es rápido y crea un hash bastante bueno que es poco probable que cause 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 sea mejor elegir una prima grande para multiplicar en su lugar. 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 en los que a menudo se usan números no primos. En el ejemplo no- FNV más adelante, por ejemplo, he usado números que aparentemente funcionan bien, pero el valor inicial no es primo. (Sin embargo, la constante de multiplicación es primo. No sé qué tan importante es eso).

Esto es mejor que la práctica común de XORcodificar hash por dos razones principales. Supongamos que tenemos un tipo con dos intcampos:

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 utilizado actualmente por el compilador de C # para tipos anónimos.

Esta página ofrece bastantes opciones. Creo que para la mayoría de los casos lo anterior es "suficientemente bueno" y es increíblemente fácil de recordar y acertar. La alternativa FNV es similarmente simple, pero usa diferentes constantes y en XORlugar de ADDuna operación combinada. Se ve algo como el código de abajo, pero el algoritmo FNV normales opera en bytes individuales, por lo que esto requeriría la modificación de 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 estamos usando aquí es siempre para el mismo número 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, debe 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 los tipos de referencia mutables, debe 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 se basa en su código hash.
Jon Skeet
fuente
8
El algoritmo descrito en el libro que menciona es, de hecho, un poco más detallado, especialmente describe qué hacer para los diferentes tipos de datos de los campos. Por ejemplo: para campos de tipo uso largo (int) (campo ^ f >>> 32) en lugar de simplemente llamar a GetHashcode. ¿Long.GetHashCodes se implementa de esa manera?
bitbonk
13
Sí, Int64.GetHashCode hace exactamente eso. En Java eso requeriría boxeo, por supuesto. Eso me recuerda: es hora de agregar un enlace al libro ...
Jon Skeet
77
23 no es una buena opción, ya que (a partir de .net 3.5 SP1) Dictionary<TKey,TValue>supone una buena distribución del módulo de ciertos primos. Y 23 es uno de ellos. Entonces, si tiene un diccionario con Capacidad 23, solo la última contribución GetHashCodeinfluye en el código hash compuesto. Así que prefiero usar 29 en lugar de 23.
CodesInChaos
23
@CodeInChaos: solo la última contribución influye en el bucket, por lo que, en el peor de los casos, podría tener que revisar las 23 entradas del diccionario. Todavía va a verificar el código hash real de cada entrada, que será barato. Si tienes un diccionario tan pequeño, es poco probable que importe mucho.
Jon Skeet
20
@ Vajda: Usualmente uso 0 como el código hash efectivo para null, lo cual no es lo mismo que ignorar el campo.
Jon Skeet
431

Tipo anónimo

Microsoft ya proporciona un buen generador genérico de HashCode: simplemente copie sus valores de propiedad / campo a un tipo anónimo y diviértalo:

new { PropA, PropB, PropC, PropD }.GetHashCode();

Esto funcionará para cualquier número de propiedades. No usa boxeo. Simplemente usa el algoritmo ya implementado en el marco para tipos anónimos.

ValueTuple - Actualización para C # 7

Como @cactuaroid menciona 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 únicamente en la pila (sin basura):

(PropA, PropB, PropC, PropD).GetHashCode();

(Nota: la técnica original que usa 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 esto podría ser optimizado por el compilador. Sería interesante comparar estas opciones, pero La opción de tupla debe ser superior.)

Rick Love
fuente
85
Sí, la GetHashCodeimplementación anónima es muy efectiva (por cierto, es la misma que la respuesta de Jon Skeet), pero el único problema con esta solución es que genera una nueva instancia en cualquier GetHashCodellamada. Puede ser un poco excesivo, en particular en caso de acceso intensivo a grandes colecciones hash ...
digEmAll
55
@digEmAll Buen punto, no pensé en la sobrecarga de crear un nuevo objeto. La respuesta de Jon Skeet es la más eficiente y no utilizará el boxeo. (@Kumba Para resolver lo desmarcado en VB, solo use un Int64 (largo) y trúnquelo después de los cálculos.)
Rick Love
42
podría decir new { PropA, PropB, PropC, PropD }.GetHashCode()también
sehe
17
VB.NET debe usar la clave en la creación de tipos anónimos: de lo New With {Key PropA}.GetHashCode()contrario, GetHashCode no devolverá el mismo código hash para diferentes objetos con las mismas propiedades de "identificación".
David Osborne
44
@Keith en ese caso, consideraría guardar el IEnumerable como un valor de lista en algún lugar en lugar de enumerarlo cada vez que se calcula el código hash. Caclulate ToList cada vez dentro de GetHashCode podría dañar el rendimiento en muchas situaciones.
Rick Love
105

Aquí está mi ayudante de hashcode.
Su ventaja es que usa argumentos de tipo genérico y, por lo tanto, no causará boxeo:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

También tiene un método de extensión para proporcionar una interfaz fluida, por lo que puede usarlo así:

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

o así:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}
codificador nocturno
fuente
55
No es necesario por T[]separado, ya que esIEnumerable<T>
nawfal
55
Podrías refactorizar esos métodos y restringir la lógica central a una función
nawfal
12
Por cierto, 31 es un cambio y resta en la CPU, que es extremadamente rápido.
Chui Tey
44
@nightcoder podrías usar params .
ANeves
66
@ChuiTey Esto es algo que todos los Mersenne Primes tienen en común.
Pharap
63

Tengo una clase de Hashing en la biblioteca auxiliar que la uso para este propósito.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

Entonces, simplemente puedes usarlo como:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

No evalué su rendimiento, por lo que cualquier comentario es bienvenido.

Wahid Shalaly
fuente
26
Bueno, causará boxeo, si los campos son tipos de valor.
Nightcoder
55
"se puede mejorar más tarde capturando la uncheckedExcepción de desbordamiento " El objetivo principal es evitar las excepciones de desbordamiento que se desean en GetHashCode. Por lo tanto, no es incorrecto si el valor se desborda inty no duele en absoluto.
Tim Schmelter
1
Un problema con este algoritmo es que cualquier matriz llena de valores nulos siempre devolverá 0, independientemente de su longitud
Nathan Adams
2
Este método auxiliar también asigna un nuevo objeto []
James Newton-King
1
Como @NathanAdams menciona, el hecho de que nullse omita por completo podría brindarle resultados inesperados. En lugar de omitirlos, debe usar un valor constante en lugar de input[i].GetHashCode()cuándo input[i]es nulo.
David Schwartz
58

Aquí está mi clase de ayuda usando la implementación de Jon Skeet .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

Uso:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

Si desea evitar escribir un método de extensión para System.Int32:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

Todavía evita cualquier asignación de montón y se usa exactamente de la misma manera:

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

Editar (mayo de 2018): EqualityComparer<T>.Defaultgetter ahora es un JIT intrínseco: Stephen Toub menciona la solicitud de extracción en esta publicación de blog .

Şafak Gür
fuente
1
Cambiaría la línea con el operador terciario para que sea:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry
Creo que el operador ternario con obj != nullcompilará a una boxinstrucción que asignará memoria si Tes un tipo de valor. En su lugar, puede usar el obj.Equals(null)que se compilará en una llamada virtual del Equalsmétodo.
Martin Liversage
Debido this.hashCode != h. No devolvería el mismo valor.
Şafak Gür
Lo sentimos, logro eliminar mi comentario en lugar de editarlo. ¿Es más beneficioso crear una nueva estructura y luego cambiar el hashCode a non-readonly y hacer: "unchecked {this.hashCode ^ = h * 397;} devuelve esto;" ¿por ejemplo?
Erik Karlsson
La inmutabilidad tiene sus beneficios ( ¿Por qué las estructuras mutables son malas? ). Sobre el rendimiento, lo que hago es bastante barato, ya que no asigna ningún espacio en el montón.
Şafak Gür
30

.NET Standard 2.1 y superior

Si está utilizando .NET Standard 2.1 o superior, puede usar la estructura System.HashCode . Hay dos métodos para usarlo:

HashCode.Combine

El Combinemétodo puede usarse para crear un código hash, dado hasta ocho objetos.

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

El Addmétodo te ayuda a lidiar con las 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();
}

GetHashCode Made Easy

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 un buen algoritmo?

Velocidad

El algoritmo que calcula un código hash debe ser rápido. Un algoritmo simple generalmente será más rápido.

Determinista

El algoritmo de hashing debe ser determinista, es decir, dada la misma entrada, siempre debe producir la misma salida.

Reducir colisiones

El algoritmo que calcula un código hash necesita mantener las colisiones hash a un mínimo. Una colisión hash es una situación que ocurre cuando dos llamadas a GetHashCodedos objetos diferentes producen códigos hash idénticos. Tenga en cuenta que las colisiones están permitidas (algunas tienen la idea errónea de que no lo están), pero deben mantenerse al mínimo.

Una buena función hash debe asignar las entradas esperadas de la manera más uniforme posible en su rango de salida. Debe tener uniformidad.

Prevenir DoS

En .NET Core cada vez que reinicia una aplicación obtendrá diferentes códigos hash. Esta es una característica de seguridad para evitar ataques de denegación de servicio (DoS). Para .NET Framework, debe habilitar esta característica agregando el siguiente archivo App.config:

<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>

Debido a esta característica, los códigos hash nunca deben usarse fuera del dominio de aplicación en el que fueron creados, nunca deben usarse como campos clave en una colección y nunca deben persistirse.

Lea más sobre esto aquí .

¿Criptográficamente seguro?

El algoritmo no tiene que ser una función hash criptográfica . Lo que significa que no tiene que cumplir las siguientes condiciones:

  • No es factible generar un mensaje que produzca un valor hash dado
  • No es factible encontrar dos mensajes diferentes con el mismo valor hash
  • Un pequeño cambio en un mensaje debería cambiar el valor de hash de manera tan extensa que el nuevo valor de hash parece no estar correlacionado con el antiguo valor de hash (efecto de avalancha).
Muhammad Rehan Saeed
fuente
29

En la mayoría de los casos en los que Equals () compara múltiples campos, realmente no importa si su GetHash () tiene hash en un campo o en muchos. Solo tiene que asegurarse de que calcular el hash sea realmente barato ( sin asignaciones , por favor) y rápido ( sin cálculos pesados y ciertamente sin conexiones de base de datos) y que proporcione una buena distribución.

El trabajo pesado debe ser parte del método Equals (); el hash debería ser una operación muy barata para permitir llamar a Equals () en la menor cantidad de elementos posible.

Y un consejo final: no confíe en que GetHashCode () sea estable en múltiples ejecuciones de aplicaciones . Muchos tipos .Net no garantizan que sus códigos hash permanezcan igual después de un reinicio, por lo que solo debe usar el valor de GetHashCode () para estructuras de datos en memoria.

Bert Huijben
fuente
10
"En la mayoría de los casos en los que Equals () compara múltiples campos, realmente no importa si su GetHash () tiene hash en un campo o en muchos". Este es un consejo peligroso, porque para los objetos que solo difieren en los campos sin hash, obtendrá colisiones hash. Si esto sucede con frecuencia, el rendimiento de las colecciones basadas en hash (HashMap, HashSet, etc.) se degradará (hasta O (n) en el peor de los casos).
sleske
10
Esto realmente sucedió en Java: en las primeras versiones de JDK String.hashCode () solo consideraba el comienzo de la cadena; esto conduce a problemas de rendimiento si usó cadenas como claves en HashMaps que solo diferían al final (lo cual es común, por ejemplo, para URL). Por lo tanto, se modificó el algoritmo (en JDK 1.2 o 1.3, creo).
sleske
3
Si ese campo 'proporciona una buena distribución' (última parte de mi respuesta), entonces un campo es suficiente ... Si no proporciona una buena distribución , entonces (y justo entonces) necesita otro cálculo. (Por ejemplo, sólo tiene que utilizar otro campo que no proporcionan una buena distribución, o utilizar múltiples campos)
Bert Huijben
No creo que haya un problema al GetHashCoderealizar asignaciones de memoria, siempre que solo lo haga la primera vez que se use (con invocaciones posteriores que simplemente devuelven un resultado en caché). Lo importante no es que uno deba hacer grandes esfuerzos para evitar colisiones, sino que debe evitar colisiones "sistémicas". Si un tipo tiene dos intcampos oldXy con newXfrecuencia difieren en uno, un valor hash de oldX^newXasignaría el 90% de dichos valores hash de registros de 1, 2, 4 u 8. El uso de oldX+newX[aritmética no verificada] podría generar más colisiones ...
supercat
1
... de lo que sería una función más sofisticada, pero una colección de 1,000,000 de cosas que tienen 500,000 valores hash diferentes funcionará muy bien si cada valor hash tiene dos cosas asociadas, y muy mal si un valor hash tiene 500,001 cosas y los demás tienen uno cada uno.
supercat
23

Hasta hace poco, mi respuesta habría estado muy cerca de la de Jon Skeet. Sin embargo, recientemente comencé un proyecto que usaba tablas hash de potencia de dos, es decir tablas hash donde el tamaño de la tabla interna es 8, 16, 32, etc. Hay una buena razón para favorecer los tamaños de números primos, pero hay También hay algunas ventajas para los tamaños de potencia de dos.

Y casi apestaba. Entonces, después de un poco de experimentación e investigación, comencé a volver a mezclar mis hash con lo siguiente:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

Y luego mi tabla hash de poder de dos ya no apestaba.

Sin embargo, esto me molestó, porque lo anterior no debería funcionar. O más precisamente, no debería funcionar a menos que el original GetHashCode()fuera pobre de una manera muy particular.

Volver a mezclar un código hash no puede mejorar un gran código hash, porque el único efecto posible es que introducimos algunas colisiones más.

Volver a mezclar un código hash no puede mejorar un terrible código hash, porque el único efecto posible es que cambiemos, por ejemplo, una gran cantidad de colisiones en el valor 53 a una gran cantidad de valor 18,3487,291.

Remezclar un código hash solo puede mejorar un código hash que funcionó al menos bastante bien para evitar colisiones absolutas en todo su rango (2 32 valores posibles) pero mal para evitar colisiones cuando el módulo está inactivo para uso real en una tabla hash. Si bien el módulo más simple de una tabla de potencia de dos lo hizo más evidente, también estaba teniendo un efecto negativo con las tablas de números primos más comunes, eso no era tan obvio (el trabajo adicional en la repetición superaría el beneficio , pero el beneficio aún estaría allí).

Editar: también estaba usando direccionamiento abierto, lo que también habría aumentado la sensibilidad a la colisión, tal vez más que el hecho de que era poder de dos.

Y bueno, fue inquietante cuánto podrían mejorarse las string.GetHashCode()implementaciones en .NET (o estudio aquí ) de esta manera (en el orden de las pruebas que se ejecutan entre 20 y 30 veces más rápido debido a menos colisiones) y más inquietante cuánto mis propios códigos hash podría mejorarse (mucho más que eso).

Todas las implementaciones de GetHashCode () que codifiqué en el pasado, y que de hecho utilicé como la base de las respuestas en este sitio, fueron mucho peores de lo que lo había hecho . La mayor parte del tiempo fue "lo suficientemente bueno" para muchos de los usos, pero quería algo mejor.

Así que puse ese proyecto a un lado (de todos modos era un proyecto favorito) y comencé a buscar cómo producir un código hash bueno y bien distribuido en .NET rápidamente.

Al final me decidí a portar SpookyHash a .NET. De hecho, el código anterior es una versión de ruta rápida del uso de SpookyHash para producir una salida de 32 bits a partir de una entrada de 32 bits.

Ahora, SpookyHash no es un buen código rápido para recordar. Mi puerto es aún menos porque lo he insertado a mano para una mejor velocidad *. Pero para eso está la reutilización de código.

Luego puse ese proyecto a un lado, porque así como el proyecto original había producido la pregunta de cómo producir un mejor código hash, ese proyecto produjo la pregunta de cómo producir una mejor memoria .NET.

Luego regresé y produje muchas sobrecargas para alimentar fácilmente casi todos los tipos nativos (excepto decimal†) en un código hash.

Es rápido, por lo que Bob Jenkins merece la mayor parte del crédito porque su código original del que lo porté es aún más rápido, especialmente en máquinas de 64 bits para las cuales el algoritmo está optimizado ‡.

El código completo se puede ver en https://bitbucket.org/JonHanna/spookilysharp/src, pero considere que el código anterior es una versión simplificada.

Sin embargo, dado que ahora ya está escrito, uno puede usarlo más fácilmente:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

También toma valores iniciales, por lo que si necesita lidiar con datos no confiables y desea protegerse contra los ataques Hash DoS, puede establecer una semilla basada en el tiempo de actividad o similar, y hacer que los resultados sean impredecibles para los atacantes:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* Una gran sorpresa en esto es que incluyó a mano un método de rotación que devolvió (x << n) | (x >> -n)cosas mejoradas. Habría estado seguro de que la inquietud me lo habría explicado, pero el perfil mostró lo contrario.

decimalno es nativo desde la perspectiva .NET, aunque sí lo es desde C #. El problema con esto es que su propia GetHashCode()trata la precisión como significativa, mientras que la suya Equals()no lo hace. Ambas son opciones válidas, pero no se mezclan así. Al implementar su propia versión, debe elegir hacer una u otra, pero no puedo saber cuál le gustaría.

‡ A modo de comparación. Si se usa en una cadena, SpookyHash en 64 bits es considerablemente más rápido que string.GetHashCode()en 32 bits, que es ligeramente más rápido que string.GetHashCode()en 64 bits, que es considerablemente más rápido que SpookyHash en 32 bits, aunque aún lo suficientemente rápido como para ser una elección razonable.

Jon Hanna
fuente
Al combinar varios valores hash en uno, tiendo a usar longvalores para los resultados intermedios y luego reducir el resultado final a un int. ¿Te parece una buena idea? Mi preocupación es que uno usa, por ejemplo, hash = (hash * 31) + nextField, luego los pares de valores coincidentes solo afectarán los 27 bits superiores del hash. Dejar que el cálculo se extienda a una longy envolver las cosas minimizaría ese peligro.
supercat
@supercat depende de la distribución de su munging final. La biblioteca SpookilySharp aseguraría que la distribución fuera buena, idealmente (porque no necesitará la creación de objetos) al pasar un puntero a un tipo blittable o al pasar uno de los enumerables que maneja directamente, pero si aún no tiene blittable datos o una enumeración adecuada, luego llamar .Update()con los múltiples valores según la respuesta anterior hará el truco.
Jon Hanna
@ JonHanna, ¿estaría dispuesto a ser más preciso con el comportamiento problemático que encontró? Estoy tratando de implementar una biblioteca que haga que la implementación de objetos de valor sea trivial ( ValueUtils ) y me encantaría un conjunto de pruebas que demuestre poca miscibilidad de hash en tablas de hash de potencia de dos.
Eamon Nerbonne
@EamonNerbonne Realmente no tengo nada más preciso que "el tiempo general fue más lento de esa manera". Como agregué en una edición, el hecho de que estaba usando direccionamiento abierto puede haber sido más importante que el factor de potencia de dos. Planeo hacer algunos casos de prueba en un proyecto particular en el que compararé algunos enfoques diferentes, por lo que podría tener una mejor respuesta para usted después de eso, aunque eso no es una alta prioridad (un proyecto personal sin necesidad apremiante) , así que lo alcanzaré cuando lo haga ...)
Jon Hanna
@ JonHanna: sí, sé cómo va el calendario del proyecto personal, ¡buena suerte! En cualquier caso, veo que no expresé bien ese último comentario: tenía la intención de pedir la entrada problemática, y no necesariamente los detalles de los problemas que resultaron. Me encantaría usar eso como un conjunto de prueba (o inspiración para un conjunto de prueba). En cualquier caso, buena suerte con tu proyecto de mascota :-).
Eamon Nerbonne
13

Este es bueno:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

Y aquí está cómo usarlo:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}
Magnus
fuente
1
¿Cómo se determinan las llaves? GetHashCode () no toma ningún parámetro, por lo que debe llamar a este con dos claves que deben determinarse de alguna manera. Lo sentimos, sin más explicaciones, esto solo parece inteligente, pero no tan bueno.
Michael Stum
¿Y por qué necesitas las sobrecargas genéricas? El tipo no es importante (y no se usa en su código) ya que todos los objetos tienen un GetHashCode()método, por lo que siempre puede usar el método con el paramsparámetro de matriz. ¿O me estoy perdiendo algo aquí?
gehho
44
Cuando usa objetos en lugar de genéricos, obtiene asignaciones de memoria y boxeo, que no desea en GetHashCode. Entonces los genéricos son el camino a seguir.
CodesInChaos
1
El cambio de arrastre / xor pasos ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);tienen una hediondez del código: no dependen de ninguna de la entrada y mirar muy redundante para mí.
sehe
1
@Magnus, sí, borraré mi comentario original. Solo una pequeña nota de que esto puede no ser tan rápido como algunas otras soluciones aquí, pero como usted dice, no debería importar. La distribución es excelente, mejor que la mayoría de las soluciones aquí, ¡así que +1 de mi parte! :)
nawfal
11

A partir de https://github.com/dotnet/coreclr/pull/14863 , ¡hay una nueva forma de generar códigos hash que es súper simple! Solo escribe

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

Esto generará un código hash de calidad sin que tenga que preocuparse por los detalles de implementación.

James Ko
fuente
Parece una dulce adición ... ¿alguna forma de saber qué versión de .NET Core se incluirá?
Dan J
1
@DanJ Qué feliz coincidencia, los HashCodecambios para corefx se fusionaron solo un par de horas antes de tu comentario :) El tipo está programado para enviarse en .NET Core 2.1.
James Ko
Eso es asombroso, y bastante el tiempo de respuesta. Votado :)
Dan J
@DanJ Aún mejores noticias: debería estar disponible ahora mismo en las compilaciones nocturnas de CoreFX alojadas en el feed MyGet de dotnet-core.
James Ko
Sweet - que no me ayuda en el trabajo, ya que estamos no es que el sangrado de punta, pero bueno saber. ¡Salud!
Dan J
9

Aquí hay otra implementación fluida del algoritmo publicado anteriormente por Jon Skeet , pero que no incluye asignaciones ni operaciones de boxeo:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

Uso:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

El compilador se asegurará de HashValueque no se llame con una clase debido a la restricción de tipo genérico. Pero no hay soporte para el compilador HashObjectya que agregar un argumento genérico también agrega una operación de boxeo.

Scott Wegner
fuente
8

Aquí está mi enfoque simplista. Estoy usando el patrón de construcción clásico para esto. Es de tipo seguro (sin boxing / unboxing) y también compatible con .NET 2.0 (sin métodos de extensión, etc.).

Se usa así:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

Y aquí está la clase de constructor acutal:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}
bitbonk
fuente
puede evitar la creación de objetos dentro de la función gethashcode como en la respuesta de Mangus. Simplemente llame a las malditas funciones hash estáticas (a quién le importa el hash inicial). Además, podría usar el AddItems<T>(params T[] items)método con más frecuencia en la clase auxiliar (que llamar AddItem(T)cada vez).
nawfal
¿Y qué beneficio encuentra hacer this.result * Prime2 * item.GetHashCode()cuando se usa con frecuencia this.result * Prime2 + item.GetHashCode()?
nawfal
No puedo usar AddItems<T>(params T[] items)más a menudo porque typeof(T1) != typeof(T2)etc.
bitbonk
oh si, me perdí eso.
nawfal
5

Los usuarios de ReSharper pueden generar GetHashCode, Equals y otros con ReSharper -> Edit -> Generate Code -> Equality Members.

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}
Charles Burns
fuente
4

Si no tenemos más de 8 propiedades (con suerte), aquí hay otra alternativa.

ValueTuplees una estructura y parece tener una GetHashCodeimplementación sólida .

Eso significa que simplemente podríamos hacer esto:

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

Vamos a echar un vistazo a la aplicación actual de .NET Core de ValueTuple's GetHashCode.

Esto es de ValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

Y esto es de HashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

En inglés:

  • Girar a la izquierda (desplazamiento circular) h1 en 5 posiciones.
  • Sume el resultado y h1 juntos.
  • XOR el resultado con h2.
  • Comience realizando la operación anterior en {semilla aleatoria estática, h1}.
  • Para cada elemento adicional, realice la operación en el resultado anterior y el elemento siguiente (por ejemplo, h2).

Sería bueno saber más sobre las propiedades de este algoritmo de código hash ROL-5.

Lamentablemente, diferir ValueTuplepara los nuestros GetHashCodepuede no ser tan rápido como nos gustaría y esperar. Este comentario en una discusión relacionada ilustra que llamar directamente HashHelpers.Combinees más eficiente. Por otro lado, ese es interno, por lo que tendríamos que copiar el código, sacrificando gran parte de lo que habíamos ganado aquí. Además, seríamos responsables de recordar primero Combinecon la semilla aleatoria. No sé cuáles son las consecuencias si omitimos ese paso.

Timo
fuente
Suponiendo que h1 >> 27es 0 para ignorarlo, h1 << 5es igual , h1 * 32por lo tanto, es igual que h1 * 33 ^ h2. Según esta página , se llama "Bernstein modificado".
cactuaroid
3

La mayor parte de mi trabajo se realiza con la conectividad de la base de datos, lo que significa que todas mis clases tienen un identificador único de la base de datos. Siempre uso el ID de la base de datos para generar el código hash.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}
Mark G
fuente
Eso significa que si tiene objetos Persona y Cuenta y ambos tienen e ID = 1, tendrán el mismo código hash. Y eso no está bien.
pero
15
En realidad, el comentario anterior es incorrecto. Siempre habrá la posibilidad de colisiones de código hash (un código hash solo localiza el depósito, no el objeto individual). Por lo tanto, una implementación de este tipo, para un código hash que contiene objetos mixtos, provocaría muchas colisiones, lo que no es deseable, pero sería absolutamente correcto si alguna vez tuviera objetos de un solo tipo en sus tablas hash. También no distribuye uniformemente, sin embargo tampoco lo hace la implementación base de System.Object, así que no me preocuparía demasiado ...
piers7
2
El código hash puede ser solo el id, ya que el id es un número entero. No hay necesidad de llamar a GetHashCode en un entero (es una función de identidad)
Darrel Lee
2
@DarrelLee pero también su _id podría ser un Guid. Es una buena práctica de codificación, _id.GetHashCodeya que la intención es clara.
nawfal
2
@ 1224, dependiendo de los patrones de uso, puede ser horrible por la razón que da, pero también puede ser excelente; Si tiene una secuencia de tales números sin agujeros, entonces tiene un hash perfecto, mejor que cualquier algoritmo puede producir. Si sabe que ese es el caso, incluso puede contar con él y omitir el control de igualdad.
Jon Hanna
3

Bastante similar a la solución del codificador nocturno, excepto que es más fácil aumentar los números primos si lo desea.

PD: Esta es una de esas veces en las que vomitas un poco en la boca, sabiendo que esto podría ser refactorizado en un método con 9 valores predeterminados, pero sería más lento, por lo que solo cierra los ojos y trata de olvidarte.

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}
Dbl
fuente
2
No maneja nulos.
JJS
1

Me encontré con un problema con flotantes y decimales usando la implementación seleccionada como la respuesta anterior.

Esta prueba falla (flota; el hash es el mismo aunque cambié 2 valores a negativo):

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Pero esta prueba pasa (con ints):

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Cambié mi implementación para no usar GetHashCode para los tipos primitivos y parece funcionar mejor

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }
HokieMike
fuente
1
En caso de que la intención de lo contrario uncheckedNO afecta Convert.ToInt32: uint, long, float, doubley decimalpueden todos desbordamiento aquí.
Mark Hurd
1

Microsoft lidera varias formas de hash ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

Puedo adivinar que para múltiples big int puedes usar esto:

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

Y lo mismo para el tipo múltiple: todos se convierten primero en intusar, GetHashCode() luego los valores int se corregirán y el resultado será su hash.

Para aquellos que usan hash como ID (me refiero a un valor único), el hash está naturalmente limitado a un número de dígitos, creo que fueron 5 bytes para el algoritmo de hash, al menos MD5.

Puede convertir varios valores en un valor hash y algunos de ellos son iguales, así que no lo use como identificador. (tal vez algún día voy a usar su componente)

hombre muerto
fuente
77
Xoring enteros para hacer un código hash es un antipatrón bien conocido que tiende a dar lugar a un número particularmente alto de colisiones con valores del mundo real.
Jon Hanna
Todos aquí usan números enteros, y nunca ha habido ningún tipo de garantía para que el hash sea igual, solo trató de variar tanto como hay pocas colisiones.
deadManN
Sí, pero su segundo y quinto no intentan evitar colisiones.
Jon Hanna
1
Sí, ese antipatrón es bastante común.
Jon Hanna
2
Hay un equilibrio que alcanzar. Use un código de hash realmente bueno como Spookyhash y obtendrá una evitación de colisiones mucho mejor, pero tendrá mucho más tiempo de cálculo que cualquiera de estos (pero cuando se trata de cifrar grandes cantidades de datos, Spookyhash es extremadamente rápido). Un cambio simple en uno de los valores antes del xoring es solo un costo adicional marginal para una buena reducción de la colisión. La multiplicación de números primos aumenta de nuevo tanto el tiempo como la calidad. Lo que es mejor entre shift o mult es, por lo tanto, discutible. Claro xor, aunque a menudo tiene muchas colisiones en datos reales y es mejor evitarlo
Jon Hanna
1

Esta es una clase auxiliar estática que implementa la implementación de Josh Bloch; y proporciona sobrecargas explícitas para "prevenir" el boxeo, y también para implementar el hash específicamente para las primitivas largas.

Puede pasar una comparación de cadenas que coincida con su implementación igual.

Debido a que la salida de Hash siempre es un int, puede simplemente encadenar llamadas de Hash.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}
Steven Coco
fuente
Yipes: ¡Encontré un error! El HashKeysAndValuesmétodo ha sido arreglado: invoca HashKeyAndValue.
Steven Coco
0

En caso de que quiera rellenar HashCodedesdenetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

Nota: Si se usa con struct, asignará memoria debido al boxeo

Ivan Sanz-Carasa
fuente