41 Stimmen

Gute GetHashCode()-Überschreibung für eine Liste von Foo-Objekten unter Beachtung der Reihenfolge

EnumerableObject : IEnumerable<Foo>

wickelt eine List<Foo>

Si EnumerableObject a.SequenceEquals( EnumerableObject b) dann sind sie gleich.

Daher ist eine GetHashCode müssen umgesetzt werden. Das Problem ist, dass die XOR-Verknüpfung jedes Elements in der Liste den gleichen Hash-Code für jede Liste mit allen und nur den gleichen Elementen ergibt, unabhängig von der Reihenfolge. Das ist okay, wenn es funktioniert, führt aber zu vielen Kollisionen, was die Abfrage verlangsamt usw.

Was ist eine gute, schnelle GetHashCode Methode für Listen von Objekten, die von der Reihenfolge abhängig ist?

75voto

Jon Skeet Punkte 1325502

Ich würde es so machen, wie ich normalerweise Hash-Codes kombiniere - mit einer Addition und einer Multiplikation:

public override int GetHashCode()
{
    unchecked
    {
        int hash = 19;
        foreach (var foo in foos)
        {
            hash = hash * 31 + foo.GetHashCode();
        }
        return hash;
    }
}

(Beachten Sie, dass Sie der Liste nichts mehr hinzufügen sollten, nachdem diese für den Schlüssel in einer beliebigen Hash-Tabelle verwendet wurde, da sich der Hash ändern wird. Dies setzt auch voraus, dass es keine Nulleinträge gibt - wenn es welche geben könnte, müssen Sie das berücksichtigen).

13voto

Jon Hanna Punkte 106367

Überprüfen Sie zunächst, ob Sie überhaupt einen Hashcode benötigen. Werden Sie diese Listen in eine Hash-Struktur (z. B. ein Wörterbuch, ein Hashset usw.) einfügen? Wenn nicht, vergessen Sie es.

In der Annahme, dass Sie meinen, dass EnumerableObject bereits überschreibt Equals(object) (und implementiert daher hoffentlich auch IEquatable<EnumerableObject> ) aus irgendeinem Grund, dann ist dies in der Tat notwendig. Sie wollen ein Gleichgewicht zwischen Geschwindigkeit und Bitverteilung herstellen.

Ein guter Ausgangspunkt ist ein mult+add oder ein shift+xor wie:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

(Dies setzt voraus, dass Sie item.Equals() für Ihre Sequenz Gleichheit Vergleich verwenden, wenn Sie eine IEqualityComparer's gleich müssen Sie in seinem Hashcode aufrufen).

Von dort aus können wir optimieren.

Wenn Nullelemente nicht zulässig sind, entfernen Sie die Null-Prüfung (Vorsicht, dies führt dazu, dass der Code ausfällt, wenn er jemals eine Null findet).

Wenn sehr große Listen üblich sind, müssen wir die Anzahl der untersuchten Listen reduzieren, ohne dass es zu vielen Kollisionen kommt. Vergleichen Sie die folgenden unterschiedlichen Implementierungen:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int max = Math.Min(Count, 16);
    for(int i = 0, i != max; ++i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int min = Math.Max(-1, Count - 16);
    for(int i = Count -1, i != min; --i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int step = Count / 16 + 1;
    for(int i = 0, i < Count; i += step)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

Dadurch wird die Gesamtzahl der untersuchten Elemente begrenzt, was die Ausführung beschleunigt, aber das Risiko einer schlechteren Qualität der Hashes birgt. Welche Methode die beste ist, hängt davon ab, ob Sammlungen mit demselben Anfang oder demselben Ende wahrscheinlicher sind.

Durch Ändern der Zahl 16 oben wird das Gleichgewicht angepasst; eine kleinere Zahl ist schneller, aber eine höhere Zahl bedeutet eine bessere Hash-Qualität und ein geringeres Risiko von Hash-Kollisionen.

Edit: Und jetzt können Sie meine Implementierung von SpookyHash v. 2 :

public override int GetHashCode()
{
  var hasher = new SpookyHash();//use methods with seeds if you need to prevent HashDos
  foreach(var item in this)
    hasher.Update(item.GetHashCode());//or relevant feeds of item, etc.
  return hasher.Final().GetHashCode();
}

Dies führt zu einer viel besseren Verteilung als mult+add oder shift+xor und ist zudem besonders schnell (insbesondere bei 64-Bit-Prozessen, da der Algorithmus dafür optimiert ist, obwohl er auch bei 32-Bit gut funktioniert).

9voto

MovGP0 Punkte 6480

En .GetHashCode() Methode gibt normalerweise nur einen Hash auf der Grundlage der Objektreferenz (Zeigeradresse) zurück. Dies liegt daran, dass die Berechnung des Hash-Codes für jedes Element in einer aufzählbaren Liste sehr zeitaufwändig sein kann. Anstatt das bestehende Verhalten zu überschreiben, ziehe ich es vor, eine Erweiterungsmethode zu verwenden und diese nur dort einzusetzen, wo der Hash-Code deterministisch bestimmt werden muss:

public static class EnumerableExtensions
{
    public static int GetSequenceHashCode<TItem>(this IEnumerable<TItem> list)
    {
        if (list == null) return 0;
        const int seedValue = 0x2D2816FE;
        const int primeNumber = 397;
        return list.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item, default(TItem)) ? 0 : item.GetHashCode()));
    }
}

1voto

Ramil Shavaleev Punkte 151

Meine Erweiterungsmethode mit Nullbehandlung basiert auf Jon Skeet Antwort :

#region UTILS
/// <summary>
/// Utils
/// </summary>
internal static class UTILS
{
    #region GetHashCodeByItems
    /// <summary>
    /// Hash code depending on the content and order of the elements of the collection
    /// </summary>
    /// <param name="lst">Collection</param>
    /// <typeparam name="T">The type of items in the collection</typeparam>
    /// <returns>Hash code</returns>
    internal static int GetHashCodeByItems<T>(this IEnumerable<T> lst)
    {
        unchecked
        {
            int hash = 19;
            foreach (T item in lst)
            {
                hash = hash * 31 + (item != null ? item.GetHashCode() : 1);
            }
            return hash;
        }
    }
    #endregion
}
#endregion

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X