Sollte der Hash-Code von null in .NET immer Null sein

87

Angesichts der Tatsache, dass Sammlungen gerne als festgelegtes Mitglied System.Collections.Generic.HashSet<>akzeptieren null, kann man sich fragen, wie der Hash-Code nulllauten soll. Es sieht so aus, als würde das Framework Folgendes verwenden 0:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

Dies kann bei nullbaren Aufzählungen (ein wenig) problematisch sein. Wenn wir definieren

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

dann kann der Nullable<Season>(auch genannt Season?) nur fünf Werte annehmen, aber zwei davon, nämlich nullund Season.Spring, haben den gleichen Hash-Code.

Es ist verlockend, einen "besseren" Gleichheitsvergleich wie diesen zu schreiben:

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

Aber gibt es einen Grund, warum der Hash-Code von nullsein sollte 0?

EDIT / ADDITION:

Einige Leute scheinen zu denken, dass es um das Überschreiben geht Object.GetHashCode(). Das ist es wirklich nicht. (Die Autoren von .NET hat eine Überschreibung von machen GetHashCode()in der Nullable<>Struktur , die ist relevant, though.) Ein benutzer geschrieben Umsetzung des parameterlos GetHashCode()kann nie mit der Situation , wo das Objekt , dessen Hash - Code , den wir suchen , ist null.

Hier geht es darum, die abstrakte Methode zu EqualityComparer<T>.GetHashCode(T)implementieren oder die Schnittstellenmethode auf andere Weise zu implementieren IEqualityComparer<T>.GetHashCode(T). Jetzt, während ich diese Links zu MSDN erstelle, sehe ich, dass dort steht, dass diese Methoden ein ArgumentNullExceptionif auslösen, wenn ihr einziges Argument ist null. Dies muss sicherlich ein Fehler bei MSDN sein? Keine der .NET-eigenen Implementierungen löst Ausnahmen aus. Das Werfen in diesem Fall würde effektiv jeden Versuch unterbrechen, nullzu a hinzuzufügen HashSet<>. Es sei denn, HashSet<>es handelt sich um etwas Außergewöhnliches, wenn es um einen nullGegenstand geht (das muss ich testen).

NEUE BEARBEITUNG / ERGÄNZUNG:

Jetzt habe ich versucht zu debuggen. Mit HashSet<>, kann ich das mit dem Standardgleichheitsvergleich bestätigen, die Werte Season.Springund null werde in dem gleichen Eimer beenden. Dies kann durch sehr sorgfältige Prüfung der privaten Array-Mitglieder m_bucketsund festgestellt werden m_slots. Beachten Sie, dass die Indizes von Natur aus immer um eins versetzt sind.

Der oben angegebene Code behebt dies jedoch nicht. Wie sich herausstellt, HashSet<>wird der Gleichheitsvergleicher niemals gefragt, wann der Wert ist null. Dies ist aus dem Quellcode von HashSet<>:

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

Dies bedeutet, dass es zumindest für HashSet<>nicht einmal möglich ist, den Hash von zu ändern null. Stattdessen besteht eine Lösung darin, den Hash aller anderen Werte wie folgt zu ändern:

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}
Jeppe Stig Nielsen
quelle
1
Ich stimme dem zu - sehr gute Frage.
Sachin Kainth
26
Warum sollte der Hash-Code für null nicht null sein? Eine Hash-Kollision ist nicht das Ende der Welt.
Hot Licks
3
Abgesehen davon, dass es sich um eine bekannte, recht häufige Kollision handelt. Nicht, dass es schlecht oder gar ein großes Problem ist, es ist einfach zu vermeiden
Chris Pfohl
8
lol warum denke ich "wenn das .NET Framework von einer Brücke springt, würden Sie ihm folgen?" ...
Adam Houldsworth
3
Was wäre eine Nullsaison aus Neugier?
SwDevMan81

Antworten:

25

Solange der Hash - Code für NULL - Werte zurückgegeben wird , ist konsistent für die Art, sollten Sie in Ordnung sein. Die einzige Voraussetzung für einen Hash-Code ist, dass zwei Objekte, die als gleich gelten, denselben Hash-Code verwenden.

Die Rückgabe von 0 oder -1 für null funktioniert, solange Sie eine auswählen und diese ständig zurückgeben. Nicht-Null-Hash-Codes sollten natürlich nicht den Wert zurückgeben, den Sie für Null verwenden.

Ähnliche Fragen:

GetHashCode für Nullfelder?

Was sollte GetHashCode zurückgeben, wenn der Objektbezeichner null ist?

Die "Bemerkungen" dieses MSDN-Eintrags werden im Zusammenhang mit dem Hash-Code ausführlicher beschrieben. Ergreifend, wird die Dokumentation keine Abdeckung oder die Diskussion über Nullwert liefert überhaupt - nicht einmal in den Community - Inhalten.

Um Ihr Problem mit der Aufzählung zu beheben, implementieren Sie entweder den Hash-Code erneut, um einen Wert ungleich Null zurückzugeben, fügen Sie einen standardmäßigen "unbekannten" Aufzählungseintrag hinzu, der null entspricht, oder verwenden Sie einfach keine nullbaren Aufzählungen.

Interessanter Fund übrigens.

Ein weiteres Problem, das ich dabei im Allgemeinen sehe, ist, dass der Hash-Code keinen 4-Byte- oder größeren Typ darstellen kann, der ohne mindestens eine Kollision nullbar ist (mehr, wenn die Typgröße zunimmt). Beispielsweise ist der Hash-Code eines int nur der int, sodass der gesamte int-Bereich verwendet wird. Welchen Wert in diesem Bereich wählen Sie für null? Was auch immer Sie auswählen, es kollidiert mit dem Hash-Code des Werts.

Kollisionen an und für sich sind nicht unbedingt ein Problem, aber Sie müssen wissen, dass sie vorhanden sind. Hash-Codes werden nur unter bestimmten Umständen verwendet. Wie in den Dokumenten zu MSDN angegeben, wird nicht garantiert, dass Hash-Codes unterschiedliche Werte für unterschiedliche Objekte zurückgeben. Dies sollte daher nicht erwartet werden.

Adam Houldsworth
quelle
Ich denke nicht, dass die Fragen, die Sie verlinken, völlig ähnlich sind. Wenn Sie Object.GetHashCode()in Ihrer eigenen Klasse (oder Struktur) überschreiben , wissen Sie, dass dieser Code nur dann getroffen wird, wenn Personen tatsächlich eine Instanz Ihrer Klasse haben. Diese Instanz kann nicht sein null. Deshalb sollten Sie nicht anfangen , Überschreibung von Object.GetHashCode()mit if (this == null) return -1;Es gibt einen Unterschied zwischen „Sein null“ und „ist ein Objekt einige Felder besitzen, die null“.
Jeppe Stig Nielsen
Sie sagen: Offensichtlich sollten Nicht-Null-Hash-Codes nicht den Wert zurückgeben, den Sie für Null verwenden. Das wäre ideal, da stimme ich zu. Und das ist der Grund, warum ich meine Frage an erster Stelle gestellt habe, denn wann immer wir eine Aufzählung schreiben T, dann (T?)nullund (T?)default(T)wird der gleiche Hash-Code (in der aktuellen Implementierung von .NET). Dies könnte geändert werden, wenn die Implementierer von .NET entweder den Hash-Code von null oder den Hash-Code-Algorithmus von ändern System.Enum.
Jeppe Stig Nielsen
Ich bin damit einverstanden, dass die Links für null interne Felder waren. Sie erwähnen, dass es sich um IEqualityComparer <T> handelt. In Ihrer Implementierung ist der Hash-Code immer noch spezifisch für einen Typ, sodass Sie sich immer noch in der gleichen Situation befinden, Konsistenz für den Typ. Die Rückgabe des gleichen Hash-Codes für Nullen eines beliebigen Typs spielt keine Rolle, da Nullen keinen Typ haben.
Adam Houldsworth
1
Hinweis: Ich habe meine Frage zweimal aktualisiert. Es stellt sich heraus, dass es (zumindest mit HashSet<>) nicht funktioniert, den Hash-Code von zu ändern null.
Jeppe Stig Nielsen
6

Denken Sie daran, dass der Hash-Code nur als erster Schritt zur Bestimmung der Gleichheit verwendet wird und niemals als De-facto-Bestimmung verwendet wird, ob zwei Objekte gleich sind.

Wenn die Hash-Codes von zwei Objekten nicht gleich sind, werden sie als ungleich behandelt (weil wir davon ausgehen, dass die nicht zugrunde liegende Implementierung korrekt ist - dh wir raten nicht darüber nach). Wenn sie denselben Hash-Code haben, sollten sie auf tatsächliche Gleichheit überprüft werden. In Ihrem Fall nullschlagen der und der Enum-Wert fehl.

Infolgedessen ist die Verwendung von Null so gut wie jeder andere Wert im allgemeinen Fall.

Sicher, es wird Situationen wie Ihre Aufzählung geben, in denen diese Null mit dem Hash-Code eines echten Werts geteilt wird . Die Frage ist, ob für Sie der winzige Aufwand eines zusätzlichen Vergleichs Probleme verursacht.

Wenn ja, definieren Sie Ihren eigenen Vergleicher für den Fall der Nullwert für Ihren bestimmten Typ und stellen Sie sicher, dass ein Nullwert immer einen Hashcode liefert, der (natürlich!) Immer gleich ist, und einen Wert, der vom Basiswert nicht geliefert werden kann Typ's eigener Hash-Code-Algorithmus. Für Ihre eigenen Typen ist dies machbar. Für andere - viel Glück :)

Andras Zoltan
quelle
5

Es muss nicht haben zu Null - Sie können es machen könnte 42 , wenn Sie es wollten.

Alles, was zählt, ist die Konsistenz während der Ausführung des Programms.

Dies ist nur die offensichtlichste Darstellung, da sie nullintern häufig als Null dargestellt wird. Wenn Sie also beim Debuggen einen Hash-Code von Null sehen, werden Sie möglicherweise aufgefordert zu denken: "Hmm ... war dies ein Null-Referenzproblem?"

Beachten Sie, dass, wenn Sie eine Zahl wie verwenden 0xDEADBEEF, jemand sagen könnte, dass Sie eine magische Zahl verwenden ... und Sie wären es auch. (Man könnte sagen, Null ist auch eine magische Zahl, und Sie hätten irgendwie Recht ... außer dass sie so weit verbreitet ist, dass sie eine Ausnahme von der Regel darstellt.)

user541686
quelle
4

Gute Frage.

Ich habe gerade versucht, dies zu codieren:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

und führen Sie dies wie folgt aus:

Season? v = null;
Console.WriteLine(v);

es kehrt zurück null

wenn ja, stattdessen normal

Season? v = Season.Spring;
Console.WriteLine((int)v);

es kehrt 0wie erwartet zurück oder einfacher Frühling, wenn wir es vermeiden, zu werfen int.

Also .. wenn Sie Folgendes tun:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

BEARBEITEN

Von MSDN

Wenn zwei Objekte gleich sind, muss die GetHashCode-Methode für jedes Objekt denselben Wert zurückgeben. Wenn jedoch zwei Objekte nicht gleich verglichen werden, müssen die GetHashCode-Methoden für die beiden Objekte keine unterschiedlichen Werte zurückgeben

Mit anderen Worten: Wenn zwei Objekte denselben Hash - Code haben, bedeutet nicht , dass sie gleich sind, weil wirkliche Gleichheit bestimmt wird durch Equals .

Nochmals von MSDN:

Die GetHashCode-Methode für ein Objekt muss konsistent denselben Hashcode zurückgeben, solange der Objektstatus, der den Rückgabewert der Equals-Methode des Objekts bestimmt, nicht geändert wird. Beachten Sie, dass dies nur für die aktuelle Ausführung einer Anwendung gilt und dass ein anderer Hash-Code zurückgegeben werden kann, wenn die Anwendung erneut ausgeführt wird.

Tigran
quelle
6
Eine Kollision bedeutet per Definition, dass zwei ungleiche Objekte denselben Hashcode haben. Sie haben gezeigt, dass die Objekte nicht gleich sind. Haben sie jetzt den gleichen Hash-Code? Laut OP ist dies eine Kollision. Jetzt ist es nicht das Ende der Welt, eine Kollision zu haben, es ist einfach eine wahrscheinlichere Kollision, als wenn null auf etwas anderes als 0 gehasht wird, was die Leistung beeinträchtigt.
Servy
1
Was sagt Ihre Antwort eigentlich? Sie sagen, dass Season.Spring nicht gleich null ist. Nun, das ist nicht falsch, aber es beantwortet die Frage in keiner Weise wirklich, oder?
Servy
2
@Servy: Die Frage lautet: Warum habe ich den gleichen Hascode für 2 verschiedene Objekte ( null und Spring ). Die Antwort ist also, dass es keine Kollisionsursache gibt, selbst wenn sie denselben Hashcode haben, sie sind übrigens nicht gleich.
Tigran
3
"Antwort: warum nicht?" Nun, das OP hat Ihre Frage "Warum nicht" präventiv beantwortet. Es ist wahrscheinlicher, Kollisionen zu verursachen als eine andere Zahl. Er fragte sich, ob es einen Grund gab, warum 0 gewählt wurde, und bisher hat niemand darauf geantwortet.
Servy
1
Diese Antwort enthält nichts, was das OP noch nicht weiß. Dies geht aus der Art und Weise hervor, wie die Frage gestellt wurde.
Konrad Rudolph
4

Aber gibt es einen Grund, warum der Hash-Code von Null 0 sein sollte?

Es hätte überhaupt alles sein können. Ich stimme eher zu, dass 0 nicht unbedingt die beste Wahl war, aber es ist eine, die wahrscheinlich zu den wenigsten Fehlern führt.

Eine Hash-Funktion muss unbedingt denselben Hash für denselben Wert zurückgeben. Sobald es eine Komponente gibt, die dies tut, ist dies wirklich der einzig gültige Wert für den Hash von null. Wenn es dafür eine Konstante gäbe, wie hm, object.HashOfNulldann IEqualityComparermüsste jemand, der eine implementiert , wissen, um diesen Wert zu verwenden. Wenn sie nicht darüber nachdenken, ist die Wahrscheinlichkeit, dass sie 0 verwenden, etwas höher als jeder andere Wert, denke ich.

Zumindest für HashSet <> ist es nicht einmal möglich, den Hash von null zu ändern

Wie oben erwähnt, denke ich, dass es völlig unmöglich ist, einen Punkt zu machen, nur weil es Typen gibt, die bereits der Konvention folgen, dass der Hash von Null 0 ist.

Roman Starkov
quelle
Wenn man die Methode EqualityComparer<T>.GetHashCode(T)für einen bestimmten Typ implementiert, der Tes erlaubt null, muss man etwas tun , wenn das Argument ist null. Sie könnten (1) werfen ArgumentNullException, (2) zurückkehren 0oder (3) etwas anderes zurückgeben. Ich nehme Ihre Antwort für eine Empfehlung, 0in dieser Situation immer zurückzukehren ?
Jeppe Stig Nielsen
@JeppeStigNielsen Ich bin mir nicht sicher, ob es um Wurf oder Rückkehr geht, aber wenn Sie sich für eine Rückkehr entscheiden, dann definitiv Null.
Roman Starkov
2

Der Einfachheit halber ist es 0. Es gibt keine so harte Anforderung. Sie müssen nur die allgemeinen Anforderungen der Hash-Codierung sicherstellen.

Zum Beispiel müssen Sie sicherstellen , dass , wenn zwei Objekte gleich sind, ihre Hashcodes muss immer gleich sein. Daher müssen unterschiedliche Hashcodes immer unterschiedliche Objekte darstellen (dies ist jedoch nicht unbedingt umgekehrt: Zwei unterschiedliche Objekte haben möglicherweise denselben Hashcode, auch wenn dies häufig vorkommt, ist dies keine Hash-Funktion von guter Qualität - sie hat keine gute Kollisionsfestigkeit).

Natürlich habe ich meine Antwort auf Anforderungen mathematischer Natur beschränkt. Es gibt auch .NET-spezifische technische Bedingungen, die Sie hier lesen können . 0 für einen Nullwert gehört nicht dazu.

Thomas Calc
quelle
1

Dies könnte also durch die Verwendung eines UnknownEnum-Werts vermieden werden (obwohl es etwas seltsam erscheint Season, wenn a unbekannt ist). So etwas würde dieses Problem negieren:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

Dann hätten Sie eindeutige Hash-Code-Werte für jede Saison.

SwDevMan81
quelle
1
ja, aber dies beantwortet eigentlich nicht die Frage. Auf diese Weise wird laut Frage null mit Uknown kollidieren. Was ist ein Unterschied?
Tigran
@Tigran - Diese Version verwendet keinen
nullbaren
Ich verstehe, aber die Frage betrifft den nullbaren Typ.
Tigran
Ich habe eine Million Mal eine Szene auf SO, die die Leute als Antwort auf Verbesserungsvorschläge anbieten.
SwDevMan81
1

Persönlich finde ich die Verwendung von nullbaren Werten etwas umständlich und versuche, sie zu vermeiden, wann immer ich kann. Ihr Problem ist nur ein weiterer Grund. Manchmal sind sie zwar sehr praktisch, aber meine Faustregel lautet, Werttypen möglichst nicht mit Null zu mischen, nur weil diese aus zwei verschiedenen Welten stammen. In .NET Framework scheinen sie dasselbe zu tun - viele Werttypen bieten eine TryParseMethode, mit der Werte von keinem Wert getrennt werden können ( null).

In Ihrem speziellen Fall ist es einfach, das Problem zu beseitigen, da Sie mit Ihrem eigenen SeasonTyp umgehen .

(Season?)nullFür mich bedeutet "Saison ist nicht angegeben", wie wenn Sie ein Webformular haben, in dem einige Felder nicht benötigt werden. Meiner Meinung nach ist es besser, diesen speziellen 'Wert' an sich anzugeben, enumals etwas klobig zu verwenden Nullable<T>. Es ist schneller (kein Boxen) leichter zu lesen ( Season.NotSpecifiedvs null) und löst Ihr Problem mit Hash-Codes.

Natürlich ist es für andere Typen, wie intSie die Wertdomäne nicht erweitern können, nicht immer möglich, einen der Werte als speziell zu bezeichnen. Aber mit int?Hash-Code ist die Kollision, wenn überhaupt, ein viel kleineres Problem.

Maciej
quelle
Wenn Sie "Boxen" sagen, meinen Sie damit "Umschließen", dh einen Strukturwert in eine Nullable<>Struktur einfügen (auf den das HasValueMitglied dann gesetzt wird true). Sind Sie sicher, dass das Problem bei wirklich kleiner ist int?? In den meisten Fällen werden nur wenige Werte von verwendet int, und dann entspricht dies einer Aufzählung (die theoretisch viele Mitglieder haben kann).
Jeppe Stig Nielsen
Im Allgemeinen würde ich sagen, dass Enum gewählt wird, wenn eine begrenzte Anzahl bekannter Werte erforderlich ist (2-10). Wenn das Limit größer oder gar nicht ist, intist dies sinnvoller. Natürlich variieren die Vorlieben.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Denis535
quelle
1
Das ist ein interessanter Ansatz. Es wäre nützlich, Ihre Antwort so zu bearbeiten, dass sie zusätzliche Erklärungen enthält, insbesondere angesichts der Art der Frage.
Jeremy Caney