Jaki jest najlepszy algorytm do zastępowania GetHashCode?

1448

W .NET GetHashCodemetoda jest używana w wielu miejscach w bibliotekach klas podstawowych .NET. Prawidłowe wdrożenie jest szczególnie ważne, aby szybko znaleźć przedmioty w kolekcji lub określić równość.

Czy istnieje standardowy algorytm lub najlepsza praktyka w zakresie implementacji GetHashCodedla moich klas niestandardowych, aby nie obniżać wydajności?

bitbonk
źródło
38
Po przeczytaniu tego pytania i poniższego artykułu mogłem zaimplementować zastąpienie GetHashCode. Mam nadzieję, że będzie to pomocne dla innych. Wytyczne i zasady dotyczące GetHashCode napisane przez Erica Lipperta
przedłużenie
4
„lub w celu ustalenia równości”: nie! Dwa obiekty o tym samym haszu nie muszą być równe.
Thomas Levesque,
1
@ThomasLevesque Masz rację, dwa obiekty z tym samym kodem skrótu niekoniecznie są równe. Ale nadal GetHashCode()jest używany w bardzo wielu implementacjach Equals(). Właśnie to miałem na myśli z tym stwierdzeniem. GetHashCode()wewnątrz Equals()jest często używany jako skrót do określenia nierówności , ponieważ jeśli dwa obiekty mają inny kod skrótu, muszą to być obiekty, które nie są równe, a reszta kontroli równości nie musi zostać wykonana.
bitbonk
3
@bitbonk Zazwyczaj oba GetHashCode()i Equals()muszą patrzeć na wszystkie pola obu obiektów (Równe musi to zrobić, jeśli kody skrótu są równe lub niezaznaczone). Z tego powodu wezwanie do GetHashCode()środka Equals()jest często zbędne i może obniżyć wydajność. Equals()może również powodować zwarcie, co znacznie przyspiesza - jednak w niektórych przypadkach kody skrótu mogą być buforowane, co sprawia, że GetHashCode()sprawdzenie jest szybsze i bardziej opłacalne. Zobacz to pytanie, aby uzyskać więcej.
NotEnoughData
AKTUALIZACJA JAN 2020: Blog Erica Lipperta pod adresem: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

Odpowiedzi:

1603

Zwykle używam czegoś takiego jak implementacja podana we wspaniałej Effective Java Josh Blocha . Jest szybki i tworzy całkiem niezły skrót, który raczej nie spowoduje kolizji. Wybierz dwie różne liczby pierwsze, np. 17 i 23, i wykonaj:

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

Jak zauważono w komentarzach, może okazać się, że lepiej jest wybrać dużą liczbę pierwszą do pomnożenia. Najwyraźniej 486187739 jest dobry ... i chociaż większość przykładów, które widziałem z małymi liczbami, zwykle używają liczb pierwszych, istnieją co najmniej podobne algorytmy, w których często używane są liczby inne niż liczby pierwsze. Na przykład w niezupełnie FNV użyłem liczb, które najwyraźniej działają dobrze - ale początkowa wartość nie jest liczbą pierwszą. (Jednak stała mnożenia jest liczbą pierwszą. Nie wiem do końca, jak to jest ważne).

Jest to lepsze niż powszechna praktyka wprowadzania XORkodów mieszających z dwóch głównych powodów. Załóżmy, że mamy typ z dwoma intpolami:

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

Nawiasem mówiąc, wcześniejszy algorytm jest obecnie używany przez kompilator C # dla typów anonimowych.

Ta strona daje całkiem sporo opcji. Myślę, że w większości przypadków powyższe jest „wystarczająco dobre” i jest niezwykle łatwe do zapamiętania i poprawienia. FNV alternatywą jest podobnie prosty, ale stosuje różne stałe i XORzamiast ADDjako łączenie operacji. Wygląda to jak poniższy kod, ale normalny algorytm FNV działa na poszczególnych bajtach, więc wymagałoby to modyfikacji w celu wykonania jednej iteracji na bajt, zamiast na 32-bitową wartość skrótu. FNV jest również zaprojektowany dla zmiennych długości danych, podczas gdy my go tutaj używamy, zawsze dla tej samej liczby wartości pól. Komentarze do tej odpowiedzi sugerują, że kod tutaj nie działa tak dobrze (w testowanym przypadku przykładowym), jak powyższe podejście do dodawania.

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

Należy pamiętać, że jedną rzeczą, o której należy pamiętać, jest to, że najlepiej zapobiegać zmianie stanu wrażliwego na równouprawnienie (a tym samym hashcode) po dodaniu go do kolekcji zależnej od kodu skrótu.

Zgodnie z dokumentacją :

Możesz zastąpić GetHashCode dla niezmiennych typów referencji. Zasadniczo w przypadku zmiennych typów referencyjnych należy zastąpić GetHashCode tylko wtedy, gdy:

  • Możesz obliczyć kod skrótu z pól, które nie są zmienne; lub
  • Możesz upewnić się, że kod skrótu zmiennego obiektu nie zmienia się, gdy obiekt jest zawarty w kolekcji zależnej od jego kodu skrótu.
Jon Skeet
źródło
8
Algorytm opisany w książce, o której wspominasz, jest w rzeczywistości nieco bardziej szczegółowy, w szczególności opisuje, co zrobić dla różnych typów danych pól. Np .: dla pól typu long use (int) (field ^ f >>> 32) zamiast po prostu wywołać GetHashcode. Czy long.GetHashCodes jest zaimplementowane w ten sposób?
bitbonk
13
Tak, Int64.GetHashCode robi dokładnie to. W Javie wymagałoby to oczywiście boksu. To mi przypomina - czas dodać link do książki ...
Jon Skeet
77
23 nie jest dobrym wyborem, ponieważ (od .net 3.5 SP1) Dictionary<TKey,TValue>zakłada dobry rozkład modulo pewnych liczb pierwszych. A 23 jest jednym z nich. Więc jeśli masz słownik z Pojemność 23, tylko ostatni wkład w GetHashCodewpływanie na złożony hashcode. Wolę więc użyć 29 zamiast 23.
CodesInChaos
23
@CodeInChaos: Tylko ostatni wkład wpływa na koszyk - więc może, w najgorszym przypadku, przejrzeć wszystkie 23 wpisy w słowniku. Nadal sprawdzi rzeczywisty kod skrótu każdego wpisu, który będzie tani. Jeśli masz tak mały słownik, prawdopodobnie nie będzie to miało większego znaczenia.
Jon Skeet
20
@Vajda: Zwykle używam 0 jako efektywnego kodu skrótu null- co nie jest tym samym, co ignorowanie pola.
Jon Skeet
431

Typ anonimowy

Microsoft już zapewnia dobry ogólny generator HashCode: Po prostu skopiuj wartości swojej właściwości / pola do anonimowego typu i haszuj go:

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

Będzie to działać dla dowolnej liczby właściwości. Nie używa boksu. Po prostu używa algorytmu już zaimplementowanego w ramach dla typów anonimowych.

ValueTuple - aktualizacja dla C # 7

Jak wspomniano w komentarzach @cactuaroid, można użyć krotki wartości. Oszczędza to kilka naciśnięć klawiszy i, co ważniejsze, wykonuje się wyłącznie na stosie (bez śmieci):

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

(Uwaga: Oryginalna technika wykorzystująca anonimowe typy wydaje się tworzyć obiekt na stercie, tj. Śmieci, ponieważ anonimowe typy są implementowane jako klasy, choć kompilator może to zoptymalizować. Ciekawe byłoby przetestowanie tych opcji, ale opcja krotki powinna być lepsza.)

Rick Love
źródło
85
Tak, anonimowa GetHashCodeimplementacja jest bardzo skuteczna (BTW jest taka sama jak ta w odpowiedzi Jona Skeeta), ale jedynym problemem z tym rozwiązaniem jest generowanie nowej instancji przy każdym GetHashCodewywołaniu. Może to być nieco narzut, szczególnie w przypadku intensywnego dostępu do dużych kolekcji z haszowaniem ...
digEmAll
5
@digEmAll Dobra uwaga, nie myślałem o narzutach związanych z tworzeniem nowego obiektu. Odpowiedź Jona Skeeta jest najbardziej wydajna i nie będzie używać boksu. (@Kumba Aby rozwiązać niezaznaczone w VB, wystarczy użyć Int64 (długi) i obciąć go po obliczeniach.)
Rick Love
42
mógłbym new { PropA, PropB, PropC, PropD }.GetHashCode()też powiedzieć
patrz
17
VB.NET musi używać klucza do tworzenia anonimowego typu: New With {Key PropA}.GetHashCode()W przeciwnym razie GetHashCode nie zwróci tego samego kodu skrótu dla różnych obiektów o tych samych właściwościach „identyfikujących”.
David Osborne,
4
@Keith w takim przypadku rozważałbym zapisanie gdzieś IEnumerable jako wartości listy zamiast wyliczania go za każdym razem, gdy obliczany jest kod skrótu. Wyliczanie ToList za każdym razem w GetHashCode może zaszkodzić wydajności w wielu sytuacjach.
Rick Love,
105

Oto mój pomocnik hashcode.
Zaletą jest to, że używa argumentów typu ogólnego i dlatego nie powoduje boksowania:

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

Ma również metodę rozszerzenia, aby zapewnić płynny interfejs, dzięki czemu można go używać w następujący sposób:

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

lub tak:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}
nightcoder
źródło
5
Nie ma potrzeby T[]osobnego, ponieważ jest jużIEnumerable<T>
nawfal
5
Można refaktoryzować te metody i ograniczyć podstawową logikę do jednej funkcji
nawfal
12
Nawiasem mówiąc, 31 to przesunięcie i odjęcie procesora, który jest niezwykle szybki.
Chui Tey
4
@nightcoder można użyć params .
ANeves
6
@ChuiTey To jest coś, co łączy wszystkie Mersenne Primes .
Pharap
63

Mam klasę Hashing w bibliotece Pomocnika, której używam do tego celu.

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

Następnie możesz po prostu użyć go jako:

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

Nie oceniłem jego wydajności, więc wszelkie opinie są mile widziane.

Wahid Shalaly
źródło
26
To spowoduje boks, jeśli pola są typami wartości.
nightcoder
5
„można poprawić później, wychwytując uncheckedwyjątek przepełnienia”. Chodzi o to, aby uniknąć wyjątków dotyczących przepełnienia, które są pożądane GetHashCode. Więc nie jest niepoprawne, jeśli wartość się przepełnia inti wcale nie boli.
Tim Schmelter
1
Jednym z problemów związanych z tym algorytmem jest to, że każda tablica pełna wartości null zawsze zwróci 0, niezależnie od jego długości
Nathan Adams
2
Ta metoda pomocnicza przydziela także nowy obiekt []
James Newton-King
1
Jak wspomina @NathanAdams, fakt, że nullzostał całkowicie pominięty, może dać nieoczekiwane rezultaty. Zamiast ich pomijać, powinieneś użyć stałej wartości zamiast input[i].GetHashCode()kiedy input[i]null.
David Schwartz
58

Oto moja klasa pomocnicza wykorzystująca implementację Jona Skeeta .

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

Stosowanie:

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

Jeśli chcesz uniknąć pisania metody rozszerzenia dla 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;
}

Nadal unika się alokacji sterty i jest używany dokładnie w ten sam sposób:

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

Edycja (maj 2018): EqualityComparer<T>.Defaultgetter jest teraz nieodłączną częścią JIT - prośba o ściągnięcie jest wspomniana przez Stephena Touba w tym poście na blogu .

Şafak Gür
źródło
1
Zmieniłbym linię z trzeciorzędnym operatorem na:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry
Wierzę, że operator potrójny z obj != nullskompiluje się do boxinstrukcji, która przydzieli pamięć, jeśli Tjest typem wartości. Zamiast tego możesz użyć, obj.Equals(null)który skompiluje się do wirtualnego wywołania Equalsmetody.
Martin Liversage,
Ponieważ this.hashCode != h. Nie zwróci tej samej wartości.
Şafak Gür
Niestety, udało mi się usunąć mój komentarz zamiast go edytować. Czy bardziej korzystne jest utworzenie nowej struktury, a następnie zmiana kodu skrótu na nie tylko do odczytu i wykonanie: „niezaznaczone {this.hashCode ^ = h * 397;} zwróć to;” na przykład?
Erik Karlsson,
Niezmienność ma swoje zalety ( dlaczego zmienne struktury są złe? ). Jeśli chodzi o wydajność, to, co robię, jest dość tanie, ponieważ nie przydziela miejsca na stercie.
Şafak Gür
30

.NET Standard 2.1 i wyżej

Jeśli używasz .NET Standard 2.1 lub nowszej wersji, możesz użyć struktury System.HashCode . Istnieją dwie metody korzystania z niego:

HashCode.Combine

CombineMetoda może być stosowana do tworzenia kod skrótu, podane do ośmiu obiektów.

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

HashCode.Add

AddMetoda pomaga radzić sobie z kolekcji:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

Łatwe GetHashCode

Możesz przeczytać pełny wpis na blogu „ GetHashCode Made Easy ”, aby uzyskać więcej informacji i komentarzy.

Przykład użycia

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

Realizacja

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

Co sprawia, że ​​dobry algorytm?

Prędkość

Algorytm obliczający kod skrótu musi być szybki. Prosty algorytm zwykle będzie szybszy.

Deterministyczny

Algorytm mieszania musi być deterministyczny, tzn. Przy takim samym wejściu zawsze musi generować ten sam wynik.

Ogranicz kolizje

Algorytm obliczający kod skrótu musi utrzymywać kolizje skrótu na minimalnym poziomie. Kolizja skrótu to sytuacja, w której dwa wywołania GetHashCodedwóch różnych obiektów generują identyczne kody skrótu. Należy pamiętać, że kolizje są dozwolone (niektóre mają błędne przekonanie, że nie są), ale należy je ograniczyć do minimum.

Dobra funkcja skrótu powinna odwzorowywać oczekiwane dane wejściowe możliwie równomiernie w całym zakresie wyjściowym. Powinien mieć jednolitość.

Prevent's DoS

W .NET Core przy każdym ponownym uruchomieniu aplikacji otrzymasz różne kody skrótu. Jest to funkcja bezpieczeństwa zapobiegająca atakom typu Denial of Service (DoS). W przypadku .NET Framework należy włączyć tę funkcję, dodając następujący plik App.config:

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

Z powodu tej funkcji kody skrótu nigdy nie powinny być używane poza domeną aplikacji, w której zostały utworzone, nigdy nie powinny być używane jako pola kluczowe w kolekcji i nigdy nie powinny być utrwalane.

Przeczytaj więcej na ten temat tutaj .

Kryptograficznie bezpieczny?

Algorytm nie musi być kryptograficzną funkcją skrótu . Oznacza to, że nie musi spełniać następujących warunków:

  • Nie jest możliwe wygenerowanie komunikatu, który daje określoną wartość skrótu
  • Znalezienie dwóch różnych wiadomości o tej samej wartości skrótu jest niemożliwe
  • Niewielka zmiana komunikatu powinna zmienić wartość skrótu na tyle, że nowa wartość skrótu wydaje się nieskorelowana ze starą wartością skrótu (efekt lawinowy).
Muhammad Rehan Saeed
źródło
29

W większości przypadków, gdy Equals () porównuje wiele pól, tak naprawdę nie ma znaczenia, czy twoja funkcja GetHash () ma skrót na jednym polu, czy na wielu. Musisz tylko upewnić się, że obliczanie wartości skrótu jest naprawdę tanie ( bez przydziałów , proszę) i szybkie ( bez ciężkich obliczeń i na pewno żadnych połączeń z bazą danych) i zapewnia dobrą dystrybucję.

Podnoszenie ciężarów powinno być częścią metody Equals (); skrót powinien być bardzo tanią operacją, aby umożliwić wywołanie Equals () na jak najmniejszej liczbie elementów.

I ostatnia wskazówka: nie polegaj na tym, że GetHashCode () jest stabilny w wielu uruchomieniach aplikacji . Wiele typów .Net nie gwarantuje, że ich kody skrótu pozostaną takie same po ponownym uruchomieniu, więc powinieneś używać wartości GetHashCode () tylko w strukturach pamięci.

Bert Huijben
źródło
10
„W większości przypadków, w których Equals () porównuje wiele pól, tak naprawdę nie ma znaczenia, czy funkcja GetHash () ma skrót w jednym polu, czy w wielu.” Jest to niebezpieczna rada, ponieważ w przypadku obiektów, które różnią się tylko niezabudowanymi polami, wystąpią kolizje mieszające. Jeśli zdarza się to często, wydajność kolekcji opartych na haszowaniu (HashMap, HashSet itp.) Obniży się (do najgorszego przypadku do O (n)).
śleske
10
W rzeczywistości miało to miejsce w Javie: we wczesnych wersjach JDK String.hashCode () rozważał tylko początek łańcucha; prowadzi to do problemów z wydajnością, jeśli używasz ciągów jako kluczy w HashMaps, które różnią się tylko na końcu (co jest typowe np. dla adresów URL). Dlatego algorytm został zmieniony (wydaje mi się, że w JDK 1.2 lub 1.3).
śleske
3
Jeśli to jedno pole „zapewnia dobry rozkład” (ostatnia część mojej odpowiedzi), to jedno pole wystarczy. Jeśli nie zapewnia dobrego rozkładu , to (i właśnie wtedy) potrzebujesz innego obliczenia. (Np prostu użyć innego pola, które ma zapewnić dobrą dystrybucję lub używanie wielu pól)
Bert Huijben
Nie sądzę, aby wystąpił problem z GetHashCodeprzydziałem pamięci, pod warunkiem, że robi to tylko przy pierwszym użyciu (przy kolejnych wywołaniach po prostu zwraca wynik z pamięci podręcznej). Ważną rzeczą nie jest to, że należy starać się unikać kolizji, ale raczej unikać kolizji „systemowych”. Jeśli typ ma dwa intpola oldXi newXktóre często różnią się o jedną wartość hash oldX^newXbyłoby przypisanie 90% takich zapisów wartości hash 1, 2, 4 lub 8. Korzystanie oldX+newX[niezaznaczone arytmetyka] może generować więcej kolizji ...
Supercat
1
... niż bardziej wyrafinowana funkcja, ale zbiór 1 000 000 rzeczy, które mają 500 000 różnych wartości skrótu, będzie bardzo dobrze, jeśli każda wartość skrótu ma dwie powiązane rzeczy, a bardzo źle, jeśli jedna wartość skrótu ma 500 001 rzeczy, a pozostałe mają jedną.
supercat
23

Do niedawna moja odpowiedź była bardzo bliska Jona Skeeta tutaj. Jednak niedawno rozpocząłem projekt wykorzystujący potęgę dwóch tablic mieszających, czyli tabel mieszających, w których wielkość wewnętrznego stołu wynosi 8, 16, 32 itd. Jest dobry powód, aby faworyzować rozmiary liczb pierwszych, ale jest mają również zalety w stosunku do mocy dwóch rozmiarów.

I to prawie do dupy. Więc po odrobinie eksperymentów i badań zacząłem ponownie mieszać moje skróty z następującymi:

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

A potem mój stół z potęgą dwóch mocy już nie ssał.

Niepokoiło mnie to, ponieważ powyższe nie powinno działać. A dokładniej, nie powinno działać, chyba że oryginał GetHashCode()był ubogi w bardzo szczególny sposób.

Ponowne mieszanie kodu skrótu nie może poprawić świetnego kodu skrótu, ponieważ jedynym możliwym efektem jest wprowadzenie kilku dodatkowych kolizji.

Ponowne mieszanie kodu skrótu nie może poprawić okropnego kodu skrótu, ponieważ jedynym możliwym efektem jest zmiana np. Dużej liczby kolizji o wartości 53 na dużą liczbę o wartości 18.3487,291.

Ponowne mieszanie kodu skrótu może tylko poprawić kod skrótu, który co najmniej całkiem dobrze radził sobie w unikaniu bezwzględnych kolizji w całym zakresie (2 32 możliwe wartości), ale źle w unikaniu kolizji, gdy został wyłączony do faktycznego użycia w tabeli skrótów. Chociaż prostsze modulo tabeli potęgi dwóch sprawiło, że stało się to bardziej widoczne, miało to również negatywny wpływ na bardziej powszechne tabele liczb pierwszych, ale to po prostu nie było tak oczywiste (dodatkowa praca przy przerobieniu przeważałaby nad korzyścią , ale korzyść nadal byłaby dostępna).

Edycja: Używałem również otwartego adresowania, co również zwiększyłoby wrażliwość na kolizję, być może bardziej niż fakt, że była to potęga dwóch.

Cóż, niepokojące było to, w jakim stopniu string.GetHashCode()implementacje w .NET (lub studium tutaj ) mogą zostać ulepszone w ten sposób (w kolejności testów uruchamianych około 20-30 razy szybciej z powodu mniejszej liczby kolizji) i bardziej niepokojące, jak bardzo moje własne kody skrótu można poprawić (znacznie więcej).

Wszystkie implementacje GetHashCode (), które zakodowałem w przeszłości i których rzeczywiście użyłem jako podstawy odpowiedzi na tej stronie, były znacznie gorsze niż się spodziewałem . Przez większość czasu było to „wystarczająco dobre” do większości zastosowań, ale chciałem czegoś lepszego.

Dlatego odłożyłem ten projekt na bok (zresztą i tak był to projekt dla zwierząt domowych) i zacząłem szukać sposobu szybkiego stworzenia dobrego, dobrze rozproszonego kodu skrótu w .NET.

W końcu zdecydowałem się na przeniesienie SpookyHash do .NET. Rzeczywiście powyższy kod jest szybką wersją używania SpookyHash do tworzenia 32-bitowego wyjścia z 32-bitowego wejścia.

Teraz SpookyHash nie jest łatwym do zapamiętania fragmentem kodu. Mój port jest jeszcze mniejszy, ponieważ ręcznie podłożyłem dużo, aby uzyskać lepszą prędkość *. Ale po to jest ponowne użycie kodu.

Następnie odłożyłem ten projekt na bok, ponieważ tak jak w pierwotnym projekcie pojawiło się pytanie, w jaki sposób stworzyć lepszy kod skrótu, tak że w projekcie pojawiło się pytanie, w jaki sposób stworzyć lepszy memcpy .NET.

Potem wróciłem i spowodowałem wiele przeciążeń, aby łatwo wprowadzić prawie wszystkie rodzime typy (z wyjątkiem decimal†) do kodu skrótu.

Jest szybki, na co Bob Jenkins zasługuje na największe uznanie, ponieważ jego oryginalny kod, z którego się przeniosłem, jest jeszcze szybszy, szczególnie na komputerach 64-bitowych, dla których algorytm jest zoptymalizowany ‡.

Pełny kod można zobaczyć na https://bitbucket.org/JonHanna/spookilysharp/src, ale należy pamiętać, że powyższy kod jest jego uproszczoną wersją.

Ponieważ jednak jest już napisane, można z niego łatwiej korzystać:

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

Przyjmuje także wartości początkowe, więc jeśli musisz poradzić sobie z niezaufanym wejściem i chcesz chronić się przed atakami Hash DoS, możesz ustawić ziarno na podstawie czasu działania lub podobnego, a wyniki mogą być nieprzewidywalne dla atakujących:

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

* Wielką niespodzianką jest to, że ręczne wprowadzanie metody rotacji, która zwróciła (x << n) | (x >> -n)ulepszone rzeczy. Byłbym pewien, że jitter podkreśliłby to dla mnie, ale profilowanie pokazało inaczej.

decimalnie jest natywny z perspektywy .NET, choć pochodzi z C #. Problem polega na tym, że jego własna GetHashCode()traktuje precyzję jako znaczącą, podczas gdy jej własna Equals()nie. Oba są prawidłowymi wyborami, ale nie są tak mieszane. Wdrażając własną wersję, musisz wybrać jedną lub drugą, ale nie wiem, czego chcesz.

‡ Dla porównania. W przypadku użycia ciągu znaków SpookyHash na 64 bitach jest znacznie szybszy niż string.GetHashCode()na 32 bitach, co jest nieco szybszy niż string.GetHashCode()na 64 bitach, co jest znacznie szybszy niż SpookyHash na 32 bitach, choć wciąż wystarczająco szybki, aby być rozsądnym wyborem.

Jon Hanna
źródło
Łącząc wiele wartości skrótu w jedną, zwykle używam longwartości dla wyników pośrednich, a następnie munge końcowy wynik w dół do int. Czy to wydaje się dobrym pomysłem? Obawiam się, że używa się np. Hash = (hash * 31) + nextField, wtedy pary pasujących wartości wpłyną tylko na górne 27 bitów skrótu. Zezwolenie na obliczenia longi zawinięcie rzeczy zminimalizuje to niebezpieczeństwo.
supercat
@ Supercat zależy od dystrybucji twojego końcowego mungingu. Biblioteka SpookilySharp zapewniłaby dobrą dystrybucję, najlepiej (ponieważ nie będzie wymagała tworzenia obiektu), przekazując wskaźnik do typu blittable lub przekazując jeden z elementów, które obsługuje bezpośrednio, ale jeśli jeszcze nie masz blittable dane lub odpowiednie wyliczenie, a następnie wywołanie .Update()z wieloma wartościami zgodnie z powyższą odpowiedzią załatwi sprawę.
Jon Hanna
@JonHanna, czy chciałbyś być bardziej precyzyjny w związku z problematycznym zachowaniem? Próbuję zaimplementować bibliotekę, która sprawia, że ​​implementacja obiektów wartości jest trywialna ( ValueUtils ) i chciałbym zestaw testowy wykazujący słabą mieszalność mieszania w potęgach dwóch tablic mieszających.
Eamon Nerbonne
@EamonNerbonne Nie mam nic bardziej precyzyjnego niż „całkowity czas był wolniejszy”. Jak dodałem w edycji, fakt, że korzystałem z otwartego adresowania, mógł być ważniejszy niż czynnik potęgi dwóch. Planuję zrobić kilka przypadków testowych w konkretnym projekcie, w których porównuję kilka różnych podejść, więc mogę mieć dla ciebie lepszą odpowiedź, chociaż nie jest to priorytet (osobisty projekt bez pilnej potrzeby , więc dojdę do tego, kiedy dojdę do tego ...)
Jon Hanna
@JonHanna: tak, wiem, jak idzie osobisty harmonogram projektu - powodzenia! W każdym razie widzę, że nie sformułowałem dobrze tego ostatniego komentarza: chciałem poprosić o problematyczne dane wejściowe, a niekoniecznie o szczegóły powstałych problemów. Chciałbym użyć tego jako zestawu testowego (lub inspiracji dla zestawu testowego). W każdym razie - powodzenia w projekcie dla Twojego zwierzaka :-).
Eamon Nerbonne
13

Ten jest dobry:

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

A oto jak go użyć:

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
źródło
1
Jak określa się klucze? GetHashCode () nie przyjmuje żadnych parametrów, więc musi wywołać ten z dwoma kluczami, które należy jakoś określić. Niestety, bez dalszych wyjaśnień, wygląda to tylko sprytnie, ale nie tak dobrze.
Michael Stum
I dlaczego potrzebujesz ogólnych przeciążeń? Typ nie jest ważny (i nie jest używany w kodzie), ponieważ wszystkie obiekty mają GetHashCode()metodę, więc zawsze możesz użyć tej metody z paramsparametrem tablica. A może coś tu brakuje?
gehho,
4
Gdy użyjesz obiektu zamiast ogólnych, otrzymasz boks i przydziały pamięci, których nie chcesz w GetHashCode. Tak więc leki generyczne są do zrobienia.
CodesInChaos
1
Przesunięcie tylnej / xor kroki ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);mają zapachy kodu: nie zależą od dowolnego wejścia i wyglądają strasznie redundantny do mnie.
sehe
1
@Magnus tak, usunę mój oryginalny komentarz. Tylko mała uwaga, że ​​może to nie być tak szybkie, jak niektóre inne rozwiązania tutaj, ale jak mówisz, nie powinno to mieć znaczenia. Dystrybucja jest świetna, lepsza niż większość rozwiązań tutaj, więc daj +1 ode mnie! :)
nawfal
11

Począwszy od https://github.com/dotnet/coreclr/pull/14863 , istnieje nowy sposób generowania kodów skrótu, który jest bardzo prosty! Tylko napisz

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

Spowoduje to wygenerowanie wysokiej jakości kodu skrótu bez konieczności martwienia się o szczegóły implementacji.

James Ko
źródło
To wygląda na słodki dodatek ... jakikolwiek sposób, aby dowiedzieć się, która wersja .NET Core zostanie dostarczona?
Dan J
1
@DanJ Co za szczęśliwy zbieg okoliczności, HashCodezmiany w corefx zostały scalone na kilka godzin przed twoim komentarzem :) Ten typ ma się pojawić w .NET Core 2.1.
James Ko
To jest niesamowite - i dość długi czas realizacji. Pozytywne. :)
Dan J
@DanJ Jeszcze lepsza wiadomość - powinna być dostępna już teraz w nocnych wersjach CoreFX hostowanych na kanale MyNet Dotnet-Core.
James Ko
Słodko - to nie pomaga mi w pracy, ponieważ nie jesteśmy aż tak nowatorscy, ale dobrze wiedzieć. Twoje zdrowie!
Dan J
9

Oto kolejna płynna implementacja algorytmu opublikowanego powyżej przez Jona Skeeta , ale która nie obejmuje alokacji ani operacji bokserskich:

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

Stosowanie:

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

Kompilator zapewni, że HashValuenie zostanie wywołany z klasą ze względu na ogólne ograniczenie typu. Ale nie ma wsparcia dla kompilatora, HashObjectponieważ dodanie ogólnego argumentu dodaje również operację boksu.

Scott Wegner
źródło
8

Oto moje uproszczone podejście. Używam do tego klasycznego wzorca konstruktora. Jest bezpieczny dla typów (bez boxowania / rozpakowywania), a także kompatybilny z .NET 2.0 (bez metod rozszerzenia itp.).

Używa się go w następujący sposób:

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

A oto klasa klasycznego budowniczego:

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
źródło
możesz uniknąć tworzenia obiektów wewnątrz funkcji gethashcode, jak w odpowiedzi Mangusa. Wystarczy wywołać cholerne statyczne funkcje skrótu (kogo to obchodzi z hasłem startowym). Ponadto możesz AddItems<T>(params T[] items)częściej używać metody w klasie pomocniczej (niż wywoływać za AddItem(T)każdym razem).
nawfal
A jakie korzyści przynoszą this.result * Prime2 * item.GetHashCode()często używane this.result * Prime2 + item.GetHashCode()?
nawfal
Nie mogę używać AddItems<T>(params T[] items)częściej, ponieważ typeof(T1) != typeof(T2)itp.
bitbonk
o tak, tęskniłem za tym.
nawfal
5

Użytkownicy ReSharper mogą generować GetHashCode, Equals i inne za pomocą 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
źródło
4

Jeśli mamy nie więcej niż 8 właściwości (mam nadzieję), oto kolejna alternatywa.

ValueTuplejest strukturą i wydaje się mieć solidną GetHashCodeimplementację.

Oznacza to, że możemy po prostu to zrobić:

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

Rzućmy okiem na obecnej implementacji NET rdzenia za ValueTuple„s GetHashCode.

To jest z 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);
    }

A to z 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;
        }
    }

Po angielsku:

  • Obrót w lewo (przesunięcie kołowe) h1 o 5 pozycji.
  • Dodaj wynik i h1 razem.
  • XOR wynik z h2.
  • Zacznij od wykonania powyższej operacji na {static random seed seed, h1}.
  • Dla każdego kolejnego elementu wykonaj operację na poprzednim wyniku i następnym elemencie (np. H2).

Byłoby miło wiedzieć więcej o właściwościach tego algorytmu kodu skrótu ROL-5.

Niestety odroczenie się ValueTuplew naszym przypadku GetHashCodemoże nie być tak szybkie, jak byśmy tego oczekiwali. Ten komentarz w powiązanej dyskusji pokazuje, że bezpośrednie wywoływanie HashHelpers.Combinejest bardziej wydajne. Z drugiej strony, ten jest wewnętrzny, więc musielibyśmy skopiować kod, poświęcając wiele z tego, co tutaj zyskaliśmy. Bylibyśmy także odpowiedzialni za pamiętanie Combineo losowym nasieniu. Nie wiem, jakie będą konsekwencje pominięcia tego kroku.

Timo
źródło
Zakładając, że h1 >> 27zignorujesz to 0, h1 << 5jest h1 * 32więc równe h1 * 33 ^ h2. Według tej strony nazywa się to „Zmodyfikowany Bernstein”.
kaktusowiec
3

Większość mojej pracy polega na łączności z bazą danych, co oznacza, że ​​wszystkie moje klasy mają unikalny identyfikator z bazy danych. Zawsze używam identyfikatora z bazy danych, aby wygenerować kod skrótu.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}
Mark G.
źródło
Oznacza to, że jeśli masz obiekty Osoba i Konto, a oba mają i ID = 1, będą miały ten sam kod skrótu. I to nie jest w porządku.
pero
15
Właściwie powyższy komentarz jest niepoprawny. Zawsze będzie możliwość kolizji kodu skrótu (kod skrótu lokalizuje tylko segment, a nie pojedynczy obiekt). Tak więc taka implementacja - dla kodu mieszającego zawierającego obiekty mieszane - prowadziłaby do wielu kolizji, co jest niepożądane, ale byłoby absolutnie w porządku, gdybyś miał tylko obiekty jednego typu w swoich tabelach skrótów. Również nie rozkłada się równomiernie, jednak podstawowa implementacja w system.object nie byłaby tym zbytnio
zmartwiona
2
Kod skrótu może być tylko identyfikatorem, ponieważ identyfikator jest liczbą całkowitą. Nie ma potrzeby wywoływania GetHashCode na liczbie całkowitej (jest to funkcja tożsamości)
Darrel Lee
2
@DarrelLee, ale tomo jego _id może być Guid. Dobrą praktyką kodowania jest robienie tego, _id.GetHashCodeco jest jasne.
nawfal
2
@ 1224 w zależności od wzorców użytkowania może być okropny z podanego powodu, ale może też być świetny; jeśli masz ciąg takich liczb bez dziur, to masz idealny skrót, lepszy niż jakikolwiek algorytm może wygenerować. Jeśli wiesz, że tak jest, możesz nawet na to liczyć i pominąć kontrolę równości.
Jon Hanna
3

Prawie podobne do rozwiązania nightcodera, tyle że łatwiej jest podnieść liczby pierwsze, jeśli chcesz.

PS: To jeden z tych momentów, w których rzygasz trochę w usta, wiedząc, że można to zmienić na jedną z 9 domyślnych metod, ale byłoby wolniejsze, więc po prostu zamknij oczy i spróbuj o tym zapomnieć.

/// <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
źródło
2
Nie obsługuje zer.
JJS,
1

Wystąpił problem z liczbami zmiennoprzecinkowymi i dziesiętnymi przy użyciu implementacji wybranej jako odpowiedź powyżej.

Ten test kończy się niepowodzeniem (liczba zmiennoprzecinkowa; skrót jest taki sam, mimo że zmieniłem 2 wartości na ujemne):

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

Ale ten test przechodzi (z 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));

Zmieniłem implementację, aby nie używać GetHashCode dla typów pierwotnych i wydaje się, że działa lepiej

    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
źródło
1
W przypadku, gdy przeznaczone są inaczej uncheckednie wpływa na Convert.ToInt32: uint, long, float, doublei decimalwszystko może przepełnienie tutaj.
Mark Hurd
1

Microsoft prowadzi na kilka sposobów mieszania ...

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

Domyślam się, że dla wielu dużych int możesz użyć tego:

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;

To samo dotyczy wielu typów: wszystkie przekonwertowane najpierw na intużycie, GetHashCode() a następnie wartości int zostaną xor'owane, a wynikiem będzie twój skrót.

Dla tych, którzy używają skrótu jako ID (mam na myśli unikalną wartość), skrót jest naturalnie ograniczony do kilku cyfr, myślę, że było to 5 bajtów dla algorytmu skrótu, przynajmniej MD5.

Możesz zamienić wiele wartości na wartość mieszaną, a niektóre z nich są takie same, więc nie używaj jej jako identyfikatora. (może kiedyś użyję twojego komponentu)

deadManN
źródło
7
Xoring liczb całkowitych, aby utworzyć hashcode, jest dobrze znanym anty-wzorem, który prowadzi do szczególnie dużej liczby kolizji z rzeczywistymi wartościami.
Jon Hanna
Każdy tutaj używa liczby całkowitej i nigdy nie było żadnej gwarancji, że hasz będzie taki sam, po prostu starał się być tak różny, jak to tylko możliwe, że zdarzy się kilka kolizji.
deadManN
Tak, ale twój drugi i piąty nie starają się unikać kolizji.
Jon Hanna
1
Tak, ten antypattern jest dość powszechny.
Jon Hanna,
2
Jest równowaga do osiągnięcia. Użyj naprawdę dobrego kodu skrótu, takiego jak Spookyhash, a uzyskasz znacznie, znacznie lepsze unikanie kolizji, ale będzie on miał znacznie więcej czasu obliczeniowego niż którykolwiek z nich (ale jeśli chodzi o mieszanie bardzo dużych ilości danych, Spookyhash jest niezwykle szybki). Proste przesunięcie jednej z wartości przed xoringiem jest jedynie marginalnym dodatkowym kosztem dla dobrego zmniejszenia kolizji. Mnożenie liczb pierwszych ponownie zwiększa czas i jakość. To, co jest lepsze między zmianą a multiwersją, jest zatem dyskusyjne. Zwykły xor choć bardzo często ma wiele kolizji z prawdziwymi danymi i najlepiej go unikać
Jon Hanna
1

Jest to statyczna klasa pomocnicza, która implementuje implementację Josha Blocha; i zapewnia wyraźne przeciążenia, aby „zapobiec” boksowaniu, a także implementować skrót specjalnie dla długich operacji podstawowych.

Możesz przekazać ciąg znaków, który pasuje do twojej równej implementacji.

Ponieważ wyjście Hash jest zawsze int, możesz po prostu łączyć wywołania 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
źródło
Yipes: Znalazłem błąd! HashKeysAndValuesMetoda została ustalona: to wywołuje HashKeyAndValue.
Steven Coco,
0

W przypadku, gdy chcesz PolyFill HashCodeodnetstandard2.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;
    }
}

Uwaga: Jeśli zostanie użyty z struct, przydzieli pamięć ze względu na boks

Ivan Sanz-Carasa
źródło