Da ich keine Antwort finden konnte, die erklärt, warum wir überschreiben sollten GetHashCode
und Equals
für benutzerdefinierte Strukturen und warum die Standardimplementierung "wahrscheinlich nicht als Schlüssel in einer Hash-Tabelle geeignet ist", werde ich einen Link zu diesem Blog hinterlassen Beitrag , der erklärt, warum mit einem realen Beispiel eines aufgetretenen Problems.
Ich empfehle, den gesamten Beitrag zu lesen, aber hier ist eine Zusammenfassung (Hervorhebung und Klarstellung hinzugefügt).
Grund, warum der Standard-Hash für Strukturen langsam und nicht sehr gut ist:
Die Art und Weise der CLR ausgeführt ist, jeder Anruf an ein Mitglied der Definition in System.ValueType
oder System.Enum
Typen [kann] Ursache einer Box - Zuordnung [...]
Ein Implementierer einer Hash-Funktion steht vor einem Dilemma: Machen Sie eine gute Verteilung der Hash-Funktion oder machen Sie sie schnell. In einigen Fällen ist es möglich , sie beide zu erreichen, aber es ist schwer , dies zu tun allgemein in ValueType.GetHashCode
.
Die kanonische Hash-Funktion einer Struktur "kombiniert" Hash-Codes aller Felder. Der einzige Weg, um einen Hash-Code eines Feldes in einer ValueType
Methode zu erhalten, ist die Verwendung von Reflektion . Also, um den Handel Geschwindigkeit über die Verteilung der CLR Autoren entschieden und die Standard - GetHashCode
Version gibt nur einen Hash - Code eines ersten Nicht-Null - Feld und „munges“ es mit einem Typ - ID [...] Dies ist ein vernünftiges Verhalten , wenn es nicht ist . Zum Beispiel, wenn Sie Pech haben , und das erste Feld Ihrer Struktur sind den gleichen Wert für die meisten Fälle, wird eine Hash - Funktion das gleiche Ergebnis liefert die ganze Zeit. Und wie Sie sich vorstellen können, führt dies zu drastischen Auswirkungen auf die Leistung, wenn diese Instanzen in einem Hash-Set oder einer Hash-Tabelle gespeichert werden.
[...] Die reflexionsbasierte Implementierung ist langsam . Sehr langsam.
[...] Beide ValueType.Equals
und ValueType.GetHashCode
haben eine spezielle Optimierung. Wenn ein Typ keine "Zeiger" hat und [...] ordnungsgemäß gepackt ist, werden optimalere Versionen verwendet: GetHashCode
Iteriert über eine Instanz und XORs-Blöcke mit 4 Bytes, und die Equals
Methode vergleicht zwei Instanzen mit memcmp
. [...] Die Optimierung ist jedoch sehr schwierig. Erstens ist es schwer zu wissen, wann die Optimierung aktiviert ist [...] Zweitens liefert ein Speichervergleich nicht unbedingt die richtigen Ergebnisse . Hier ist ein einfaches Beispiel: [...] -0.0
und +0.0
sind gleich, haben aber unterschiedliche binäre Darstellungen.
In der Post beschriebenes Problem der realen Welt:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Wir haben ein Tupel verwendet, das eine benutzerdefinierte Struktur mit Standard-Gleichheitsimplementierung enthielt. Und leider hatte die Struktur ein optionales erstes Feld, das fast immer gleich [leere Zeichenfolge] war . Die Leistung war in Ordnung, bis die Anzahl der Elemente im Satz erheblich anstieg, was zu einem echten Leistungsproblem führte. Die Initialisierung einer Sammlung mit Zehntausenden von Elementen dauerte Minuten.
Um die Frage zu beantworten, "in welchen Fällen ich meine eigene packen sollte und in welchen Fällen ich mich sicher auf die Standardimplementierung verlassen kann", sollten Sie zumindest bei Strukturen überschreiben Equals
und GetHashCode
wann immer Ihre benutzerdefinierte Struktur als verwendet werden könnte Geben Sie eine Hash-Tabelle ein oder Dictionary
.
Ich würde auch empfehlen, IEquatable<T>
in diesem Fall zu implementieren , um Boxen zu vermeiden.
Wie die anderen Antworten sagten, ist beim Schreiben einer Klasse der Standard-Hash mit Referenzgleichheit normalerweise in Ordnung, daher würde ich mich in diesem Fall nicht darum kümmern, es sei denn, Sie müssen überschreiben Equals
(dann müssten Sie GetHashCode
entsprechend überschreiben ).