Se il codice hash di null è sempre zero, in .NET


87

Dato che le raccolte come System.Collections.Generic.HashSet<>accept nullas a set member, ci si può chiedere quale nulldovrebbe essere il codice hash di . Sembra che il framework utilizzi 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

Questo può essere (un po ') problematico con enumerazioni nullable. Se definiamo

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

quindi il Nullable<Season>(chiamato anche Season?) può assumere solo cinque valori, ma due di essi, ovvero nulle Season.Spring, hanno lo stesso codice hash.

Si è tentati di scrivere un comparatore di uguaglianza "migliore" come questo:

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

Ma c'è qualche motivo per cui nulldovrebbe essere il codice hash 0?

MODIFICA / AGGIUNTA:

Alcune persone sembrano pensare che si tratti di override Object.GetHashCode(). In realtà non lo è. (Gli autori di .NET hanno fatto un override di GetHashCode()nella Nullable<>struttura che è rilevante, però.) Un'implementazione scritta dall'utente senza parametri GetHashCode()non può mai gestire la situazione in cui si trova l'oggetto di cui cerchiamo il codice hash null.

Si tratta di implementare il metodo astratto EqualityComparer<T>.GetHashCode(T)o altrimenti implementare il metodo di interfaccia IEqualityComparer<T>.GetHashCode(T). Ora, durante la creazione di questi collegamenti a MSDN, vedo che lì si dice che questi metodi lanciano un ArgumentNullExceptionse il loro unico argomento è null. Questo deve essere certamente un errore su MSDN? Nessuna delle implementazioni di .NET genera eccezioni. Lanciare in quel caso spezzerebbe efficacemente qualsiasi tentativo di aggiungere nulla HashSet<>. A meno HashSet<>che non faccia qualcosa di straordinario quando si tratta di un nulloggetto (dovrò testarlo).

NUOVA MODIFICA / AGGIUNTA:

Ora ho provato a eseguire il debug. Con HashSet<>, posso confermare che con l'operatore di confronto di uguaglianza predefinito, i valori Season.Springe null finiranno nello stesso bucket. Ciò può essere determinato esaminando molto attentamente i membri dell'array privato m_bucketse m_slots. Notare che gli indici sono sempre, per impostazione predefinita, compensati di uno.

Il codice che ho fornito sopra, tuttavia, non risolve questo problema. A quanto pare, HashSet<>non chiederà mai al comparatore di uguaglianza quando il valore è null. Questo è dal codice sorgente di 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; 
    }

Ciò significa che, almeno per HashSet<>, non è nemmeno possibile cambiare l'hash di null. Invece, una soluzione è cambiare l'hash di tutti gli altri valori, in questo modo:

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

1
Secondo me - domanda molto buona.
Sachin Kainth

26
Perché il codice hash per null non dovrebbe essere zero? Una collisione di hashish non è la fine del mondo, sai.
Hot Licks

3
Tranne che è una collisione ben nota, abbastanza comune. Non che sia brutto o anche un problema così grave, è facilmente evitabile
Chris Pfohl,

8
lol perché sto pensando "se il framework .NET salta da un ponte, lo
seguiresti

3
Solo per curiosità, quale sarebbe una stagione nulla?
SwDevMan81

Risposte:


25

Finché il codice hash restituito per i null è coerente per il tipo, dovresti stare bene. L'unico requisito per un codice hash è che due oggetti considerati uguali condividano lo stesso codice hash.

Restituire 0 o -1 per null, purché ne sceglia uno e lo restituisca sempre, funzionerà. Ovviamente, i codici hash non nulli non dovrebbero restituire qualsiasi valore tu usi per nullo.

Domande simili:

GetHashCode sui campi nulli?

Cosa dovrebbe restituire GetHashCode quando l'identificatore dell'oggetto è nullo?

Le "Note" di questa voce MSDN vengono fornite più in dettaglio sul codice hash. Acutamente, la documentazione non fornisce alcuna copertura o discussione di valori nulli affatto - neppure nel contenuto della comunità.

Per risolvere il problema con l'enum, reimplementare il codice hash per restituire un valore diverso da zero, aggiungere una voce enum predefinita "sconosciuta" equivalente a null o semplicemente non utilizzare enumerazioni nullable.

Scoperta interessante, comunque.

Un altro problema che vedo con questo in genere è che il codice hash non può rappresentare un tipo di 4 byte o più grande che è annullabile senza almeno una collisione (di più all'aumentare della dimensione del tipo). Ad esempio, il codice hash di un int è solo l'int, quindi utilizza l'intero intervallo int. Quale valore in quell'intervallo scegli per null? Qualunque cosa tu scelga, entrerà in collisione con il codice hash del valore stesso.

Le collisioni in sé e per sé non sono necessariamente un problema, ma devi sapere che ci sono. I codici hash vengono utilizzati solo in alcune circostanze. Come indicato nei documenti su MSDN, non è garantito che i codici hash restituiscano valori diversi per oggetti diversi, quindi non ci si dovrebbe aspettare.


Non credo che le domande che colleghi siano completamente simili. Quando esegui l'override Object.GetHashCode()nella tua classe (o struttura), sai che questo codice verrà colpito solo quando le persone hanno effettivamente un'istanza della tua classe. Quell'istanza non può essere null. Ecco perché non inizi il tuo override di Object.GetHashCode()con if (this == null) return -1;C'è una differenza tra "essere null" e "essere un oggetto che possiede alcuni campi che sono null".
Jeppe Stig Nielsen

Tu dici: Ovviamente, i codici hash non nulli non dovrebbero restituire qualsiasi valore tu usi per nullo. Sarebbe l'ideale, sono d'accordo. E questo è il motivo per cui ho posto la mia domanda in primo luogo, perché ogni volta che scriviamo un enum T, allora (T?)nulle (T?)default(T)avremo lo stesso codice hash (nell'attuale implementazione di .NET). Ciò potrebbe essere cambiato se gli implementatori di .NET modificassero il codice hash null o l'algoritmo del codice hash di System.Enum.
Jeppe Stig Nielsen

Sono d'accordo che i collegamenti fossero per campi interni nulli. Dici che è per IEqualityComparer <T>, nella tua implementazione il codice hash è ancora specifico per un tipo, quindi sei ancora nella stessa situazione, coerenza per il tipo. Restituire lo stesso codice hash per null di qualsiasi tipo non avrà importanza poiché i null non hanno un tipo.
Adam Houldsworth

1
Nota: ho aggiornato due volte la mia domanda. Si scopre che (almeno con HashSet<>) non funziona cambiare il codice hash di null.
Jeppe Stig Nielsen

6

Tieni presente che il codice hash viene utilizzato solo come primo passo per determinare l'uguaglianza, e [non è / dovrebbe] mai (essere) usato per determinare de facto se due oggetti sono uguali.

Se i codici hash di due oggetti non sono uguali, vengono trattati come non uguali (perché presumiamo che l'implementazione sottostante sia corretta, ovvero non lo indoviniamo). Se hanno lo stesso codice hash, allora dovrebbero essere controllati per l' effettiva uguaglianza che, nel tuo caso, nulle il valore enum falliranno.

Di conseguenza, l'uso di zero vale quanto qualsiasi altro valore nel caso generale.

Certo, ci saranno situazioni, come il tuo enum, in cui questo zero è condiviso con il codice hash di un valore reale . La domanda è se, per te, il minuscolo sovraccarico di un confronto aggiuntivo causi problemi.

In tal caso, definisci il tuo operatore di confronto per il caso del nullable per il tuo tipo particolare e assicurati che un valore null produca sempre un codice hash che è sempre lo stesso (ovviamente!) E un valore che non può essere restituito dal sottostante algoritmo del codice hash del tipo. Per i tuoi tipi, questo è fattibile. Per gli altri - buona fortuna :)


5

Non deve essere zero : potresti farne 42 se lo desideri.

Tutto ciò che conta è la coerenza durante l'esecuzione del programma.

È solo la rappresentazione più ovvia, perché nullspesso è rappresentata internamente come zero. Il che significa che, durante il debug, se vedi un codice hash pari a zero, potrebbe chiederti: "Hmm .. era un problema di riferimento nullo?"

Nota che se usi un numero come 0xDEADBEEF, qualcuno potrebbe dire che stai usando un numero magico ... e tu lo saresti. (Potresti dire che anche lo zero è un numero magico, e avresti ragione ... tranne per il fatto che è così ampiamente usato da essere in qualche modo un'eccezione alla regola.)


4

Buona domanda.

Ho appena provato a codificare questo:

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

ed esegui questo in questo modo:

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

ritorna null

se lo faccio, invece normale

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

ritorna 0, come previsto, o semplice primavera se evitiamo di lanciare a int.

Quindi .. se fai quanto segue:

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

MODIFICARE

Da MSDN

Se due oggetti sono uguali, il metodo GetHashCode per ogni oggetto deve restituire lo stesso valore. Tuttavia, se due oggetti non vengono confrontati come uguali, i metodi GetHashCode per i due oggetti non devono restituire valori diversi

In altre parole: se due oggetti hanno lo stesso codice hash, ciò non significa che siano uguali, perché l' uguaglianza reale è determinata da Equals .

Di nuovo da MSDN:

Il metodo GetHashCode per un oggetto deve restituire in modo coerente lo stesso codice hash a condizione che non vi siano modifiche allo stato dell'oggetto che determinano il valore restituito del metodo Equals dell'oggetto. Si noti che questo è vero solo per l'esecuzione corrente di un'applicazione e che è possibile restituire un codice hash diverso se l'applicazione viene eseguita di nuovo.


6
una collisione, per definizione, significa che due oggetti disuguali hanno lo stesso codice hash. Hai dimostrato che gli oggetti non sono uguali. Ora hanno lo stesso codice hash? Secondo l'OP lo fanno, il che significa che questa è una collisione. Ora, non è la fine del mondo avere una collisione, è semplicemente una collisione più probabile rispetto a un hash null con qualcosa di diverso da 0, il che danneggia le prestazioni.
Servy

1
Quindi cosa dice effettivamente la tua risposta? Dici che Season.Spring non è uguale a zero. Bene, non è sbagliato, ma non risponde in alcun modo alla domanda in alcun modo ora lo fa.
Servy

2
@Servy: la domanda dice: ecco perché ho lo stesso hascode per 2 oggetti diversi ( null e Spring ). Quindi la risposta è che non c'è causa di collisione anche con lo stesso codice hash, tra l'altro non sono uguali.
Tigran

3
"Risposta: perché no?" Bene, l'OP ha risposto preventivamente alla tua domanda "perché no". È più probabile che causi collisioni rispetto a un altro numero. Si chiedeva se ci fosse una ragione per cui è stato scelto 0, e finora nessuno gli ha risposto.
Servy

1
Questa risposta non contiene nulla che l'OP non sappia già, evidente dal modo in cui è stata posta la domanda.
Konrad Rudolph

4

Ma c'è qualche motivo per cui il codice hash di null dovrebbe essere 0?

Potrebbe essere stato qualsiasi cosa. Tendo ad essere d'accordo sul fatto che 0 non fosse necessariamente la scelta migliore, ma è quella che probabilmente porta al minor numero di bug.

Una funzione hash deve assolutamente restituire lo stesso hash per lo stesso valore. Una volta che esiste un componente che fa questo, questo è davvero l'unico valore valido per l'hash di null. Se ci fosse una costante per questo, come, hm object.HashOfNull, allora qualcuno che implementa un IEqualityComparerdovrebbe sapere di usare quel valore. Se non ci pensano, la possibilità che utilizzino 0 è leggermente superiore a qualsiasi altro valore, credo.

almeno per HashSet <>, non è nemmeno possibile cambiare l'hash di null

Come accennato in precedenza, penso che sia completamente impossibile il punto, solo perché esistono tipi che seguono già la convenzione secondo cui l'hash di null è 0.


Quando si implementa il metodo EqualityComparer<T>.GetHashCode(T)per un tipo particolare Tche lo consente null, si deve fare qualcosa quando l'argomento è null. Puoi (1) lanciare un ArgumentNullException, (2) restituire 0o (3) restituire qualcos'altro. Prendo la tua risposta per una raccomandazione a tornare sempre 0in quella situazione?
Jeppe Stig Nielsen

@JeppeStigNielsen Non sono sicuro di lancio vs ritorno, ma se scegli di tornare, allora sicuramente zero.
Roman Starkov

2

È 0 per semplicità. Non esiste un requisito così difficile. Devi solo garantire i requisiti generali della codifica hash.

Ad esempio, devi assicurarti che se due oggetti sono uguali, anche i loro hashcode devono essere sempre uguali. Pertanto, codici hash diversi devono sempre rappresentare oggetti diversi (ma non è necessariamente vero viceversa: due oggetti diversi possono avere lo stesso codice hash, anche se questo accade spesso, questa non è una funzione hash di buona qualità - non ha un buona resistenza alle collisioni).

Ovviamente ho limitato la mia risposta a requisiti di natura matematica. Esistono anche condizioni tecniche specifiche per .NET, che puoi leggere qui . 0 per un valore nullo non è tra questi.


1

Quindi questo potrebbe essere evitato usando un Unknownvalore enum (anche se sembra un po 'strano che Seasona sia sconosciuto). Quindi qualcosa del genere annullerebbe questo problema:

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

Quindi avresti valori di codice hash univoci per ogni stagione.


1
sì, ma questo non risponde effettivamente alla domanda. In questo modo secondo la domanda null si colliderà con Uknown. Cos'è una differenza?
Tigran

@Tigran - Questa versione non utilizza un tipo nullable
SwDevMan81

Capisco, ma la domanda riguarda il tipo nullable.
Tigran

Ho una scena un milione di volte su SO che le persone offrono suggerimenti per il miglioramento come risposte.
SwDevMan81

1

Personalmente trovo che l'utilizzo di valori nullable sia un po 'imbarazzante e cerco di evitarli ogni volta che posso. Il tuo problema è solo un altro motivo. A volte sono molto utili, ma la mia regola pratica è di non mescolare i tipi di valore con null, se possibile, semplicemente perché provengono da due mondi diversi. In .NET framework sembrano fare lo stesso: molti tipi di valore forniscono un TryParsemetodo che è un modo per separare i valori da nessun valore ( null).

Nel tuo caso particolare è facile sbarazzarti del problema perché gestisci il tuo Seasontipo.

(Season?)nullper me significa "stagione non specificata" come quando si dispone di un modulo web in cui alcuni campi non sono obbligatori. Secondo me è meglio specificare quello speciale "valore" in enumsé piuttosto che usare un po 'goffo Nullable<T>. Sarà più veloce (senza boxe) più facile da leggere ( Season.NotSpecifiedvs null) e risolverà il tuo problema con i codici hash.

Ovviamente per altri tipi, come intnon è possibile espandere il dominio del valore e denominare uno dei valori come speciale non è sempre possibile. Ma con la int?collisione del codice hash è un problema molto più piccolo, se non del tutto.


Quando dici "boxing", penso che intendi "wrapping", cioè mettere un valore di struttura all'interno di una Nullable<>struttura (dove il HasValuemembro sarà quindi impostato true). Sei sicuro che il problema sia davvero più piccolo con int?? La maggior parte delle volte si usano solo pochi valori di int, quindi è equivalente a un enum (che in teoria può avere molti membri).
Jeppe Stig Nielsen

In genere direi che enum viene scelto quando è richiesto un numero limitato di valori noti (2-10). Se il limite è maggiore o nessuno, intha più senso. Ovviamente le preferenze variano.
Maciej

0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2

1
È un approccio interessante. Sarebbe utile modificare la tua risposta per includere qualche spiegazione aggiuntiva, soprattutto vista la natura della domanda.
Jeremy Caney
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.