Qual è l'algoritmo migliore per sovrascrivere GetHashCode?


1449

In .NET, il GetHashCodemetodo viene utilizzato in molti punti nelle librerie di classi base di .NET. L'implementazione corretta è particolarmente importante per trovare rapidamente gli elementi in una raccolta o quando si determina l'uguaglianza.

Esiste un algoritmo standard o best practice su come implementare le GetHashCodemie classi personalizzate in modo da non degradare le prestazioni?


38
Dopo aver letto questa domanda e l'articolo qui sotto, ho potuto implementare l'override di GetHashCode. Spero che sarebbe utile per gli altri. Linee guida e regole per GetHashCode scritte da Eric Lippert
rene il

4
"o per determinare l'uguaglianza": no! Due oggetti con lo stesso hashcode non sono necessariamente uguali.
Thomas Levesque,

1
@ThomasLevesque Hai ragione, due oggetti con lo stesso codice hash non sono necessariamente uguali. Ma GetHashCode()è ancora usato in moltissime implementazioni di Equals(). Questo è ciò che intendevo con quella frase. GetHashCode()inside Equals()è spesso usato come scorciatoia per determinare la disuguaglianza , perché se due oggetti hanno un codice hash diverso devono essere oggetti che non sono uguali e il resto del controllo di uguaglianza non deve essere eseguito.
Bitbonk,

3
@bitbonk Di solito, entrambi GetHashCode()e hanno Equals()bisogno di guardare tutti i campi di entrambi gli oggetti (Equals deve farlo se gli hashcode sono uguali o non controllati). Per questo motivo, una chiamata GetHashCode()all'interno Equals()è spesso ridondante e potrebbe ridurre le prestazioni. Equals()potrebbe anche essere in grado di cortocircuitare, rendendolo molto più veloce - tuttavia in alcuni casi gli hashcode possono essere memorizzati nella cache, rendendo il GetHashCode()controllo più veloce e quindi utile. Vedi questa domanda per di più.
NotEnoughData

AGGIORNAMENTO GENNAIO 2020: Blog di Eric Lippert all'indirizzo: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

Risposte:


1604

Di solito vado con qualcosa di simile all'implementazione data nel favoloso Java efficace di Josh Bloch . È veloce e crea un hash piuttosto buono che è improbabile che causi collisioni. Scegli due diversi numeri primi, ad es. 17 e 23, e fai:

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

Come notato nei commenti, potresti trovare meglio scegliere un numero primo per moltiplicare invece. Apparentemente 486187739 è buono ... e sebbene la maggior parte degli esempi che ho visto con numeri piccoli tendano ad usare numeri primi, ci sono almeno algoritmi simili in cui vengono spesso usati numeri non primi. Nell'esempio non abbastanza- FNV successivo, ad esempio, ho usato numeri che apparentemente funzionano bene, ma il valore iniziale non è un numero primo. (Tuttavia, la costante di moltiplicazione è ottima. Non so quanto sia importante.)

Questo è meglio della pratica comune di XORing hashcode per due motivi principali. Supponiamo di avere un tipo con due intcampi:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

A proposito, l'algoritmo precedente è quello attualmente utilizzato dal compilatore C # per tipi anonimi.

Questa pagina offre alcune opzioni. Penso che per la maggior parte dei casi quanto sopra sia "abbastanza buono" ed è incredibilmente facile da ricordare e fare bene. L' alternativa FNV è altrettanto semplice, ma utilizza costanti diverse e XORnon ADDcome un'operazione di combinazione. Sembra qualcosa come il codice di seguito, ma l'algoritmo FNV normale opera su singoli byte, quindi questo richiederebbe modificando effettuare un'iterazione per byte, anziché per valore hash a 32 bit. FNV è anche progettato per lunghezze variabili di dati, mentre il modo in cui lo stiamo usando qui è sempre per lo stesso numero di valori di campo. I commenti a questa risposta suggeriscono che il codice qui non funziona effettivamente (nel caso di esempio testato) come l'approccio di aggiunta sopra.

// 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;
    }
}

Nota che una cosa da tenere presente è che idealmente dovresti impedire che il tuo stato sensibile all'uguaglianza (e quindi sensibile all'hashcode) cambi dopo averlo aggiunto a una raccolta che dipende dal codice hash.

Secondo la documentazione :

Puoi sovrascrivere GetHashCode per tipi di riferimento immutabili. In generale, per i tipi di riferimento mutabili, è necessario sovrascrivere GetHashCode solo se:

  • È possibile calcolare il codice hash da campi che non sono modificabili; o
  • Puoi assicurarti che il codice hash di un oggetto mutabile non cambi mentre l'oggetto è contenuto in una raccolta che si basa sul suo codice hash.

8
L'algoritmo descritto nel libro che citi è in effetti un po 'più dettagliato e descrive in modo particolare cosa fare per i diversi tipi di dati dei campi. Ad esempio: per i campi di tipo long use (int) (field ^ f >>> 32) invece di chiamare semplicemente GetHashcode. Long.GetHashCodes è implementato in questo modo?
Bitbonk,

13
Sì, Int64.GetHashCode fa esattamente questo. In Java ciò richiederebbe la boxe, ovviamente. Questo mi ricorda - è tempo di aggiungere un link al libro ...
Jon Skeet,

77
23 non è una buona scelta, dal momento che (a partire da .net 3.5 SP1) Dictionary<TKey,TValue>assume una buona distribuzione in alcuni numeri primi. E 23 è uno di questi. Quindi se hai un dizionario con Capacità 23 solo l'ultimo contributo alle GetHashCodeinfluenze dell'hashcode composto. Quindi preferirei usare 29 anziché 23.
CodesInChaos

23
@CodeInChaos: solo l'ultimo contributo influenza il bucket - quindi, nel peggiore dei casi, potrebbe essere necessario esaminare tutte e 23 le voci del dizionario. Controllerà ancora il codice hash effettivo di ogni voce, che sarà economico. Se hai un dizionario così piccolo, è improbabile che importi molto.
Jon Skeet,

20
@Vajda: di solito uso 0 come codice hash efficace per null- che non equivale a ignorare il campo.
Jon Skeet,

431

Tipo anonimo

Microsoft fornisce già un buon generatore HashCode generico: basta copiare i valori di proprietà / campo in un tipo anonimo e l'hash:

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

Questo funzionerà per qualsiasi numero di proprietà. Non usa la boxe. Utilizza solo l'algoritmo già implementato nel framework per tipi anonimi.

ValueTuple - Aggiornamento per C # 7

Come menziona @cactuaroid nei commenti, è possibile utilizzare una tupla di valore. Ciò consente di risparmiare alcune sequenze di tasti e, cosa più importante, si esegue puramente sullo stack (no Garbage):

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

(Nota: la tecnica originale che utilizza tipi anonimi sembra creare un oggetto sull'heap, ovvero immondizia, poiché i tipi anonimi sono implementati come classi, sebbene ciò possa essere ottimizzato dal compilatore. Sarebbe interessante fare un benchmark di queste opzioni, ma il l'opzione tupla dovrebbe essere superiore.)


85
Sì, l' GetHashCodeimplementazione anonima è molto efficace (BTW è uguale a quello nella risposta di Jon Skeet), ma l'unico problema con questa soluzione è che si genera una nuova istanza a qualsiasi GetHashCodechiamata. Può essere un po 'sovraccarico, in particolare in caso di accesso intensivo a grandi raccolte con hash ...
digEmAll

5
@digEmAll Un buon punto, non ho pensato al sovraccarico di creare un nuovo oggetto. La risposta di Jon Skeet è la più efficiente e non utilizzerà la boxe. (@Kumba Per risolvere il non selezionato in VB, basta usare un Int64 (lungo) e troncarlo dopo i calcoli.)
Rick Love

42
poteva solo dire new { PropA, PropB, PropC, PropD }.GetHashCode()troppo
sehe

17
VB.NET deve utilizzare la chiave nella creazione di un tipo anonimo: in New With {Key PropA}.GetHashCode()caso contrario GetHashCode non restituirà lo stesso hashcode per oggetti diversi con le stesse proprietà di "identificazione".
David Osborne,

4
@Keith in quel caso, prenderei in considerazione il salvataggio di IEnumerable come valore di elenco da qualche parte invece di enumerarlo ogni volta che viene calcolato l'hashcode. Fare clic su ToList ogni volta all'interno di GetHashCode potrebbe compromettere le prestazioni in molte situazioni.
Rick Love,

105

Ecco il mio aiutante hashcode.
Il vantaggio è che utilizza argomenti di tipo generico e quindi non provoca boxe:

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

Inoltre ha un metodo di estensione per fornire un'interfaccia fluida, quindi puoi usarla in questo modo:

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

o in questo modo:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}

5
Non è necessario T[]separatamente come è giàIEnumerable<T>
nawfal

5
Potresti riformattare questi metodi e limitare la logica di base a una funzione
nawfal,

12
Per inciso, 31 è uno spostamento e sottrazione sulla CPU, che è estremamente veloce.
Chui Tey,

4
@nightcoder potresti usare i parametri .
ANeves

6
@ChuiTey Questo è qualcosa che tutti i Mersenne Primes hanno in comune.
Pharap,

63

Ho una classe Hashing nella libreria Helper che la utilizzo a questo scopo.

/// <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;
}

Quindi, semplicemente puoi usarlo come:

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

Non ho valutato le sue prestazioni, quindi qualsiasi feedback è stato accolto con favore.


26
Bene, provocherà il pugilato, se i campi sono tipi di valore.
nightcoder

5
"può essere migliorato in seguito catturando OverflowException" Il punto centrale di tutto uncheckedè quello di evitare eccezioni su overflow che si desidera su GetHashCode. Quindi non è errato se il valore trabocca inte non danneggia affatto.
Tim Schmelter,

1
Un problema con questo algoritmo è che qualsiasi array pieno di null restituirà sempre 0, indipendentemente dalla sua lunghezza
Nathan Adams,

2
Questo metodo di supporto alloca anche un nuovo oggetto []
James Newton-King,

1
Come menziona @NathanAdams, il fatto che nullvenga completamente ignorato potrebbe darti risultati inaspettati. Invece di saltarli, dovresti semplicemente usare un valore costante anziché input[i].GetHashCode()quando input[i]è nullo.
David Schwartz,

58

Ecco la mia classe di supporto che utilizza l'implementazione di 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);
}

Se si desidera evitare di scrivere un metodo di estensione per 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;
}

Evita comunque qualsiasi allocazione di heap e viene utilizzato esattamente allo stesso modo:

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

Modifica (maggio 2018): EqualityComparer<T>.Defaultgetter è ora un JIT intrinseco - la richiesta pull è menzionata da Stephen Toub in questo post del blog .


1
Vorrei cambiare la linea con l'operatore terziario per essere:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry,

Credo che l'operatore ternario obj != nullsi compili con boxun'istruzione che alloca memoria se Tè un tipo di valore. Invece puoi usare obj.Equals(null)quale compilerà per una chiamata virtuale del Equalsmetodo.
Martin Liversage,

A causa this.hashCode != h. Non restituirebbe lo stesso valore.
Şafak Gür

Spiacenti, riesci a rimuovere il mio commento invece di modificarlo. È più utile creare una nuova struttura, quindi modificare l'hashCode in non di sola lettura e fare: "deselezionato {this.hashCode ^ = h * 397;} return this;" per esempio?
Erik Karlsson,

L'immutabilità ha i suoi benefici ( perché le strutture mutabili sono malvagie? ). Per quanto riguarda le prestazioni, quello che faccio è piuttosto economico poiché non alloca spazio nello heap.
Şafak Gür

30

.NET Standard 2.1 e versioni successive

Se si utilizza .NET Standard 2.1 o versione successiva, è possibile utilizzare la struttura System.HashCode . Esistono due metodi per usarlo:

HashCode.Combine

Il Combinemetodo può essere utilizzato per creare un codice hash, dato fino a otto oggetti.

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

HashCode.Add

Il Addmetodo ti aiuta a gestire le raccolte:

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 reso facile

Puoi leggere l'intero post del blog " GetHashCode Made Easy " per maggiori dettagli e commenti.

Esempio di utilizzo

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

Implementazione

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

Cosa rende un buon algoritmo?

Velocità

L'algoritmo che calcola un codice hash deve essere veloce. Un semplice algoritmo sarà solitamente più veloce.

Deterministico

L'algoritmo di hashing deve essere deterministico, ovvero dato lo stesso input deve sempre produrre lo stesso output.

Ridurre le collisioni

L'algoritmo che calcola un codice hash deve mantenere le collisioni hash al minimo. Una collisione di hash è una situazione che si verifica quando due chiamate a GetHashCodedue oggetti diversi producono codici hash identici. Si noti che le collisioni sono consentite (alcune hanno le idee sbagliate che non lo sono) ma dovrebbero essere ridotte al minimo.

Una buona funzione hash dovrebbe mappare gli input previsti nel modo più uniforme possibile nel suo intervallo di output. Dovrebbe avere uniformità.

Prevenire il DoS

In .NET Core ogni volta che riavvii un'applicazione otterrai diversi codici hash. Questa è una funzione di sicurezza per prevenire attacchi Denial of Service (DoS). Per .NET Framework è necessario abilitare questa funzione aggiungendo il seguente file App.config:

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

A causa di questa funzione, i codici hash non dovrebbero mai essere usati al di fuori del dominio dell'applicazione in cui sono stati creati, non dovrebbero mai essere usati come campi chiave in una raccolta e non dovrebbero mai essere persistenti.

Leggi di più qui .

Crittograficamente sicuro?

L'algoritmo non deve essere una funzione hash crittografica . Significa che non deve soddisfare le seguenti condizioni:

  • È impossibile generare un messaggio che produca un determinato valore di hash
  • È impossibile trovare due diversi messaggi con lo stesso valore di hash
  • Una piccola modifica a un messaggio dovrebbe cambiare il valore dell'hash in modo così esteso che il nuovo valore dell'hash appare non correlato al vecchio valore dell'hash (effetto valanga).

29

Nella maggior parte dei casi in cui Equals () confronta più campi, non importa se il tuo GetHash () ha un hash su un campo o su molti. Devi solo assicurarti che il calcolo dell'hash sia davvero economico ( Nessuna allocazione , per favore) e veloce ( Nessun calcolo pesante e certamente nessuna connessione al database) e fornisce una buona distribuzione.

Il sollevamento pesante dovrebbe far parte del metodo Equals (); l'hash dovrebbe essere un'operazione molto economica per consentire di chiamare Equals () sul minor numero di elementi possibile.

E un ultimo suggerimento: non fare affidamento sul fatto che GetHashCode () sia stabile su più esecuzioni di applicazioni . Molti tipi .Net non garantiscono che i loro codici hash rimangano invariati dopo il riavvio, pertanto è consigliabile utilizzare solo il valore di GetHashCode () per le strutture di dati della memoria.


10
"Nella maggior parte dei casi in cui Equals () confronta più campi, non importa se il tuo GetHash () ha un hash su un campo o su molti." Questo è un consiglio pericoloso, perché per gli oggetti che differiscono solo nei campi senza hash, si otterranno collisioni di hash. Se ciò accade frequentemente, le prestazioni delle raccolte basate su hash (HashMap, HashSet ecc.) Diminuiranno (fino a O (n) nel peggiore dei casi).
sleske,

10
Questo in realtà è accaduto in Java: nelle prime versioni di JDK String.hashCode () considerava solo l'inizio della stringa; questo porta a problemi di prestazioni se hai usato le stringhe come chiavi in ​​HashMaps che differivano solo alla fine (cosa comune ad esempio per gli URL). L'algoritmo è stato quindi modificato (in JDK 1.2 o 1.3 credo).
sleske,

3
Se quel campo "fornisce una buona distribuzione" (ultima parte della mia risposta), allora un campo è sufficiente. Se non fornisce una buona distribuzione , allora (e proprio in quel momento) hai bisogno di un altro calcolo. (Ad esempio basta usare un altro campo che non fornisce una buona distribuzione, o utilizzare più campi)
Bert Huijben

Non penso che ci sia un problema con l' GetHashCodeesecuzione delle allocazioni di memoria, a condizione che lo faccia solo la prima volta che viene utilizzato (con le successive chiamate che restituiscono semplicemente un risultato memorizzato nella cache). L'importante non è che si dovrebbe fare di tutto per evitare collisioni, ma piuttosto che si dovrebbero evitare collisioni "sistemiche". Se un tipo ha due intcampi oldXe newXche differiscono frequentemente di uno, un valore di hash oldX^newXassegnerebbe il 90% di tali valori di valori di hash di 1, 2, 4 o 8. L'uso di oldX+newX[aritmetica non selezionata] potrebbe generare più collisioni ...
supercat

1
... di una funzione più sofisticata, ma una raccolta di 1.000.000 di cose con 500.000 valori di hash diversi andrà molto bene se ogni valore di hash ha due cose associate, e molto male se un valore di hash ha 500.001 cose e gli altri ne hanno uno ciascuno.
supercat

23

Fino a poco tempo fa la mia risposta sarebbe stata molto vicina a quella di Jon Skeet qui. Tuttavia, di recente ho avviato un progetto che utilizzava tabelle hash power-of-two, ovvero tabelle hash in cui le dimensioni della tabella interna sono 8, 16, 32, ecc. C'è una buona ragione per favorire le dimensioni dei numeri primi, ma lì sono anche alcuni vantaggi della potenza di due taglie.

E praticamente succhiato. Quindi, dopo un po 'di sperimentazione e ricerca, ho iniziato a rielaborare i miei hash con quanto segue:

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

E poi la mia tabella hash power-of-two non ha più succhiato.

Questo però mi ha disturbato, perché quanto sopra non dovrebbe funzionare. O più precisamente, non dovrebbe funzionare se l'originale non GetHashCode()era scadente in un modo molto particolare.

Ri-mescolare un hashcode non può migliorare un ottimo hashcode, perché l'unico effetto possibile è che introduciamo qualche altra collisione.

Ri-mescolare un codice hash non può migliorare un codice hash terribile, perché l'unico effetto possibile è cambiare ad esempio un gran numero di collisioni sul valore 53 in un gran numero di valore 18.3487.291.

Ri-mescolare un codice hash può solo migliorare un codice hash che ha funzionato almeno abbastanza bene nell'evitare le collisioni assolute in tutto il suo intervallo (2 32 valori possibili) ma malamente nell'evitare le collisioni quando il modulo è giù per l'uso effettivo in una tabella hash. Mentre il modulo più semplice di una tabella di potenza di due ha reso questo più evidente, stava anche avendo un effetto negativo con le più comuni tabelle dei numeri primi, che non era altrettanto ovvio (il lavoro extra nel rifacimento avrebbe superato il vantaggio , ma il vantaggio sarebbe ancora lì).

Modifica: stavo anche usando l'indirizzamento aperto, che avrebbe anche aumentato la sensibilità alla collisione, forse più del fatto che fosse un potere di due.

E beh, era inquietante quanto le string.GetHashCode()implementazioni in .NET (o studio qui ) potessero essere migliorate in questo modo (sull'ordine dei test in esecuzione circa 20-30 volte più veloce a causa di minori collisioni) e più inquietante quanto i miei codici hash potrebbe essere migliorato (molto di più).

Tutte le implementazioni di GetHashCode () che avevo codificato in passato e che avevo effettivamente utilizzato come base per le risposte su questo sito, erano molto peggio di quanto avessi vissuto . Gran parte del tempo era "abbastanza buono" per gran parte degli usi, ma volevo qualcosa di meglio.

Quindi ho messo da parte quel progetto (era comunque un progetto pet) e ho iniziato a cercare rapidamente come produrre un codice hash ben distribuito in .NET.

Alla fine ho deciso di trasferire SpookyHash su .NET. In effetti il ​​codice sopra è una versione rapida dell'uso di SpookyHash per produrre un output a 32 bit da un input a 32 bit.

Ora, SpookyHash non è un bel veloce da ricordare pezzo di codice. La mia porta è ancora meno perché ne ho tracciato molto a mano per una migliore velocità *. Ma è a questo che serve il riutilizzo del codice.

Quindi ho messo da parte quel progetto, perché proprio come il progetto originale aveva prodotto la domanda su come produrre un codice hash migliore, così quel progetto ha prodotto la domanda su come produrre un memcpy .NET migliore.

Poi sono tornato e ho prodotto molti sovraccarichi per alimentare facilmente quasi tutti i tipi nativi (tranne decimal†) in un codice hash.

È veloce, per il quale Bob Jenkins merita gran parte del merito perché il suo codice originale da cui ho portato è ancora più veloce, specialmente su macchine a 64 bit per le quali l'algoritmo è ottimizzato ‡.

Il codice completo è disponibile su https://bitbucket.org/JonHanna/spookilysharp/src, ma considera che il codice sopra è una versione semplificata di esso.

Tuttavia, poiché ora è già stato scritto, è possibile utilizzarlo più facilmente:

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

Prende anche valori seed, quindi se hai bisogno di gestire input non attendibili e vuoi proteggerti dagli attacchi Hash DoS puoi impostare un seed in base al tempo di attività o simile e rendere imprevedibili i risultati degli aggressori:

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 grande sorpresa in questo è che un metodo di rotazione allineato a mano ha restituito (x << n) | (x >> -n)cose migliorate. Sarei stato sicuro che il jitter lo avrebbe sottolineato per me, ma la profilazione ha mostrato il contrario.

decimalnon è nativo dal punto di vista .NET sebbene provenga dal C #. Il problema è che la propria GetHashCode()tratta la precisione in modo significativo mentre la propria Equals()no. Entrambe sono scelte valide, ma non mescolate in questo modo. Nell'implementazione della tua versione, devi scegliere di fare l'una o l'altra, ma non posso sapere quale vorresti.

‡ A titolo di confronto. Se utilizzato su una stringa, SpookyHash su 64 bit è considerevolmente più veloce rispetto string.GetHashCode()a 32 bit, che è leggermente più veloce rispetto string.GetHashCode()a 64 bit, che è considerevolmente più veloce di SpookyHash su 32 bit, sebbene sia ancora abbastanza veloce da essere una scelta ragionevole.


Quando combino più valori di hash in uno, tendo a usare i longvalori per i risultati intermedi, quindi trascino il risultato finale in un int. Sembra una buona idea? La mia preoccupazione è che si usi ad esempio hash = (hash * 31) + nextField, quindi le coppie di valori corrispondenti influenzeranno solo i 27 bit superiori dell'hash. Lasciare che il calcolo si estenda longae avvolgere le cose minimizzerebbe quel pericolo.
supercat

@supercat dipende dalla distribuzione del tuo munging finale. La libreria SpookilySharp assicurerebbe che la distribuzione fosse buona, idealmente (perché non avrebbe bisogno della creazione di oggetti) passando un puntatore a un tipo blittable o passando uno degli enumerabili che gestisce direttamente, ma se non si dispone già di Blittable dati o un'enumerazione adatta, quindi chiamare .Update()con i valori multipli secondo la risposta sopra farà il trucco.
Jon Hanna,

@JonHanna vorresti essere più preciso con il comportamento problematico che hai riscontrato? Sto cercando di implementare una libreria che renda banale l'implementazione di oggetti valore ( ValueUtils ) e mi piacerebbe un testset che dimostri scarsa miscibilità dell'hash in hashtable di potenza di due.
Eamon Nerbonne,

@EamonNerbonne Non ho nulla di più preciso di "il tempo complessivo è stato più lento in quel modo". Come ho aggiunto in una modifica, il fatto che stavo usando l'indirizzamento aperto potrebbe essere stato più importante del fattore potenza di due. Ho in programma di fare alcuni casi di prova su un particolare progetto in cui confronterò alcuni approcci diversi, quindi dopo potrei avere una risposta migliore per te, anche se non è una priorità (un progetto personale senza necessità urgente , quindi ci arrivo quando ci arrivo ...)
Jon Hanna il

@JonHanna: sì, so come va il programma personale del progetto - buona fortuna! In ogni caso, vedo che non ho espresso bene quell'ultimo commento: intendevo chiedere l'input problematico e non necessariamente i dettagli dei problemi che ne sono derivati. Mi piacerebbe usarlo come set di test (o ispirazione per un set di test). In ogni caso, buona fortuna con il tuo progetto per animali domestici :-).
Eamon Nerbonne,

13

Questo è buono:

/// <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());
    }
}

Ed ecco come 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);
    }
}

1
Come vengono determinati i tasti? GetHashCode () non accetta alcun parametro, quindi deve chiamarlo con due chiavi che devono essere determinate in qualche modo. Siamo spiacenti, senza ulteriori spiegazioni sembra solo intelligente, ma non così buono.
Michael Stum

E perché hai bisogno dei sovraccarichi generici? Il tipo non è importante (e non viene utilizzato nel codice) poiché tutti gli oggetti hanno un GetHashCode()metodo, quindi è sempre possibile utilizzare il metodo con il paramsparametro array. O mi sto perdendo qualcosa qui?
gehho,

4
Quando utilizzi un oggetto invece dei generici otterrai box e allocazioni di memoria, che non vuoi in GetHashCode. Quindi i generici sono la strada da percorrere.
CodesInChaos,

1
I passi finali di shift / xor ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);hanno un codemell: non dipendono da nessuno degli input e mi sembrano terribilmente ridondanti.
vedi

1
@Magnus sì, eliminerò il mio commento originale. Solo una piccola nota che questo potrebbe non essere veloce come alcune altre soluzioni qui, ma come dici tu non dovrebbe importare. La distribuzione è ottima, migliore della maggior parte delle soluzioni qui, quindi +1 da parte mia! :)
nawfal il

11

A partire da https://github.com/dotnet/coreclr/pull/14863 , c'è un nuovo modo per generare codici hash che è super semplice! Scrivi e basta

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

Ciò genererà un codice hash di qualità senza doversi preoccupare dei dettagli di implementazione.


Sembra una dolce aggiunta ... un modo per sapere in quale versione di .NET Core verrà spedito?
Dan J,

1
@DanJ Che felice coincidenza, le HashCodemodifiche per corefx sono state unite solo un paio d'ore prima del tuo commento :) Il tipo è previsto per la spedizione in .NET Core 2.1.
James Ko,

È fantastico - e piuttosto il tempo di inversione di tendenza. Upvoted. :)
Dan J,

@DanJ Notizie ancora migliori: dovrebbe essere disponibile in questo momento sulle build notturne di CoreFX ospitate sul feed MyGet dotnet-core.
James Ko,

Dolce - questo non mi aiuta sul lavoro, dal momento che non siamo così sanguinanti, ma è buono a sapersi. Saluti!
Dan J

9

Ecco un'altra implementazione fluente dell'algoritmo pubblicato sopra da Jon Skeet , ma che non include allocazioni o operazioni di boxe:

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

Il compilatore assicurerà che HashValuenon venga chiamato con una classe a causa del vincolo di tipo generico. Ma non c'è supporto per il compilatore HashObjectpoiché l'aggiunta di un argomento generico aggiunge anche un'operazione di boxe.


8

Ecco il mio approccio semplicistico. Per questo sto usando il classico schema di costruzione. È typesafe (senza boxe / unboxing) e anche compatibile con .NET 2.0 (senza metodi di estensione ecc.).

Si usa così:

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

Ed ecco la classe acutal builder:

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

puoi evitare la creazione di oggetti all'interno della funzione gethashcode come nella risposta di Mangus. Basta chiamare le dannate funzioni di hash statiche (chi se ne frega di hash di avviamento). Inoltre, è possibile utilizzare il AddItems<T>(params T[] items)metodo più spesso nella classe helper (piuttosto che chiamare AddItem(T)ogni volta).
nawfal,

E quale beneficio trovi this.result * Prime2 * item.GetHashCode()quando viene usato spesso this.result * Prime2 + item.GetHashCode()?
nawfal,

Non posso usare AddItems<T>(params T[] items)più spesso perché typeof(T1) != typeof(T2)ecc.
bitbonk,

oh sì, mi è mancato.
nawfal,

5

Gli utenti di ReSharper possono generare GetHashCode, Equals e altri 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;
    }
}

4

Se non abbiamo più di 8 proprietà (si spera), ecco un'altra alternativa.

ValueTupleè una struttura e sembra avere GetHashCodeun'implementazione solida .

Ciò significa che potremmo semplicemente fare questo:

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

Diamo uno sguardo al corrente implementazione di .NET Core for ValueTuples' GetHashCode.

Questo proviene da 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);
    }

E questo proviene da 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;
        }
    }

In inglese:

  • Ruota a sinistra (spostamento circolare) h1 di 5 posizioni.
  • Aggiungi il risultato e h1 insieme.
  • XOR il risultato con h2.
  • Inizia eseguendo l'operazione sopra su {seed random statico, h1}.
  • Per ogni ulteriore elemento, eseguire l'operazione sul risultato precedente e sull'elemento successivo (ad es. H2).

Sarebbe bello sapere di più sulle proprietà di questo algoritmo di codice hash ROL-5.

Purtroppo, rimandare a ValueTuplenoi stessi GetHashCodepotrebbe non essere veloce come vorremmo e ci aspettiamo. Questo commento in una discussione correlata mostra che chiamare direttamente HashHelpers.Combineè più performante. Il rovescio della medaglia, quello è interno, quindi dovremmo copiare il codice, sacrificando gran parte di ciò che abbiamo guadagnato qui. Inoltre, saremmo responsabili di ricordarci di iniziare Combinecon il seme casuale. Non so quali siano le conseguenze se saltiamo quel passaggio.


Supponendo che h1 >> 270 lo ignori, h1 << 5equivale h1 * 32quindi a essere uguale a h1 * 33 ^ h2. Secondo questa pagina , si chiama "Bernstein modificato".
cactuaroid,

3

Gran parte del mio lavoro viene svolto con la connettività del database, il che significa che tutte le mie classi hanno un identificatore univoco dal database. Uso sempre l'ID dal database per generare l'hashcode.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}

Ciò significa che se hai oggetti Persona e Account e entrambi hanno e ID = 1, avranno lo stesso codice hash. E non va bene.
Perù

15
In realtà il commento sopra non è corretto. Ci sarà sempre la possibilità di collisioni con codice hash (un codice hash individua solo il bucket, non il singolo oggetto). Quindi un'implementazione di questo tipo - per un hashcode contenente oggetti misti - porterebbe a molte collisioni, il che è indesiderabile, ma andrebbe assolutamente bene se nei tuoi hashtable avessi solo oggetti di un solo tipo. Inoltre non si distribuisce uniformemente, tuttavia l'implementazione di base su system.object non lo è, quindi non me ne preoccuperei troppo ...
piers7

2
Il codice hash può essere solo l'id, poiché l'id è un numero intero. Non è necessario chiamare GetHashCode su un numero intero (è una funzione di identità)
Darrel Lee

2
@DarrelLee ma tomo suo _id potrebbe essere un Guid. È una buona pratica di codifica fare _id.GetHashCodeperché l'intento è chiaro.
nawfal,

2
@ 1224 a seconda dei modelli di utilizzo può essere orribile per il motivo che dai, ma può anche essere eccezionale; se hai una sequenza di tali numeri senza buchi, allora hai un hash perfetto, migliore di quanto qualsiasi algoritmo possa produrre. Se sai che è così, puoi anche contare su di esso e saltare il controllo dell'uguaglianza.
Jon Hanna,

3

Praticamente simile alla soluzione del nightcoder, tranne che è più facile aumentare i numeri primi se lo si desidera.

PS: Questa è una di quelle volte in cui ti vomiti un po 'in bocca, sapendo che questo potrebbe essere trasformato in un metodo con 9 valori predefiniti, ma sarebbe più lento, quindi chiudi gli occhi e cerchi di dimenticartene.

/// <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;
        }
    }
}

2
Non gestisce i null.
JJS,

1

Ho riscontrato un problema con float e decimali utilizzando l'implementazione selezionata come risposta sopra.

Questo test fallisce (float; l'hash è lo stesso anche se ho cambiato 2 valori in 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));

Ma questo test passa (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));

Ho modificato la mia implementazione per non utilizzare GetHashCode per i tipi primitivi e sembra funzionare meglio

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

1
Nel caso in cui destinato altrimenti uncheckednon ha effetto Convert.ToInt32: uint, long, float, doublee decimalpossono tutti troppo pieno qui.
Mark Hurd,

1

Microsoft guida per diversi modi di hashing ...

//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(); 

Posso indovinare che per più big int puoi usare questo:

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;

E lo stesso vale per i multi-tipo: tutti convertiti prima in intuso, GetHashCode()quindi i valori int saranno xor'ed e il risultato è il tuo hash.

Per coloro che usano l'hash come ID (intendo un valore univoco), l'hash è naturalmente limitato a un numero di cifre, penso che fosse 5 byte per l'algoritmo di hashing, almeno MD5.

È possibile trasformare più valori in un valore con hash e alcuni di essi sono uguali, quindi non utilizzarlo come identificatore. (forse un giorno userò il tuo componente)


7
Xoring interi per creare un hashcode è un antipattern ben noto che tende a provocare un numero particolarmente elevato di collisioni con valori del mondo reale.
Jon Hanna,

Tutti qui usano numeri interi, e non c'è mai stato alcun tipo di garanzia che l'hash sia la stessa, ha solo cercato di variare tanto quanto ci sono poche collisioni.
DeadManN

Sì, ma il tuo secondo e quinto non cercano di evitare le collisioni.
Jon Hanna,

1
Sì, quell'antipasto è abbastanza comune.
Jon Hanna,

2
C'è un equilibrio da raggiungere. Usa un codice hash davvero buono come Spookyhash e otterrai una riduzione delle collisioni molto migliore, ma avrà tempi di calcolo molto più lunghi rispetto a tutti questi (ma quando si tratta di eseguire l'hashing di grandi quantità di dati, Spookyhash è estremamente veloce). Un semplice spostamento su uno dei valori prima del xoring è solo un costo aggiuntivo marginale per una buona riduzione della collisione. Moltiplicazione dei numeri primi che aumenta sia il tempo che la qualità. Ciò che è meglio tra turno o mult è quindi discutibile. Lo xor semplice, sebbene molto spesso abbia molte collisioni su dati reali ed è meglio evitarlo
Jon Hanna,

1

Questa è una classe di supporto statica che implementa l'implementazione di Josh Bloch; e fornisce sovraccarichi espliciti per "prevenire" il pugilato, e anche per implementare l'hash appositamente per le primitive lunghe.

È possibile passare un confronto di stringhe che corrisponde all'implementazione uguale.

Poiché l'output di hash è sempre un int, puoi semplicemente concatenare le chiamate 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;
        }
    }
}

Yipes: ho trovato un bug! Il HashKeysAndValuesmetodo è stato risolto: invoca HashKeyAndValue.
Steven Coco,

0

Nel caso in cui si desideri eseguire il polyfill HashCodedanetstandard2.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: se utilizzato con struct, allocerà la memoria a causa del pugilato

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.