Si le code de hachage de null est toujours égal à zéro, dans .NET

87

Étant donné que des collections comme System.Collections.Generic.HashSet<>accept nullcomme un membre d'ensemble, on peut se demander quel nulldevrait être le code de hachage de . Il semble que le framework utilise 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

Cela peut être (un peu) problématique avec les énumérations nullables. Si nous définissons

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

alors le Nullable<Season>(également appelé Season?) ne peut prendre que cinq valeurs, mais deux d'entre elles, à savoir nullet Season.Spring, ont le même code de hachage.

Il est tentant d'écrire un «meilleur» comparateur d'égalité comme celui-ci:

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

Mais y a-t-il une raison pour laquelle le code de hachage de nulldevrait être 0?

MODIFIER / AJOUTER:

Certaines personnes semblent penser qu'il s'agit de passer outre Object.GetHashCode(). Ce n'est vraiment pas le cas, en fait. (Les auteurs de .NET ont fait une substitution de GetHashCode()dans la Nullable<>structure qui est pertinente, cependant.) Une implémentation écrite par l'utilisateur du paramètre sans paramètre GetHashCode()ne peut jamais gérer la situation où se trouve l'objet dont nous recherchons le code de hachage null.

Il s'agit d'implémenter la méthode abstraite EqualityComparer<T>.GetHashCode(T)ou d'implémenter autrement la méthode d'interface IEqualityComparer<T>.GetHashCode(T). Maintenant, en créant ces liens vers MSDN, je vois qu'il y est dit que ces méthodes lancent un ArgumentNullExceptionsi leur seul argument est null. Cela doit certainement être une erreur sur MSDN? Aucune des propres implémentations de .NET ne lève d'exceptions. Lancer dans ce cas casserait effectivement toute tentative d'ajouter nullà un HashSet<>. À moins de HashSet<>faire quelque chose d'extraordinaire lorsqu'il s'agit d'un nullobjet (je vais devoir le tester).

NOUVELLE MODIFICATION / AJOUT:

Maintenant, j'ai essayé le débogage. Avec HashSet<>, je peux confirmer qu'avec le comparateur d'égalité par défaut, les valeurs Season.Springet null se termineront dans le même compartiment. Cela peut être déterminé en inspectant très soigneusement les membres du tableau privé m_bucketset m_slots. Notez que les indices sont toujours, par conception, décalés de un.

Le code que j'ai donné ci-dessus ne résout cependant pas ce problème. En fin de compte, HashSet<>ne demandera même jamais au comparateur d'égalité quand la valeur est null. Ceci provient du code source de 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; 
    }

Cela signifie qu'au moins pour HashSet<>, il n'est même pas possible de modifier le hachage de null. Au lieu de cela, une solution consiste à modifier le hachage de toutes les autres valeurs, comme ceci:

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
la source
1
J'appuie cette - très bonne question.
Sachin Kainth
26
Pourquoi le code de hachage pour null ne devrait-il pas être nul? Une collision de hachage n'est pas la fin du monde, vous savez.
Hot Licks
3
Sauf que c'est une collision bien connue, assez courante. Non pas que ce soit grave ou même majeur d'un problème, c'est simplement facilement évitable
Chris Pfohl
8
lol pourquoi est-ce que je pense "si le framework .NET saute d'un pont, le suivriez-vous?" ...
Adam Houldsworth
3
Juste par curiosité, que serait une saison nulle?
SwDevMan81

Réponses:

25

Tant que le code de hachage renvoyé pour les valeurs nulles est cohérent pour le type, tout devrait aller. La seule exigence pour un code de hachage est que deux objets considérés comme égaux partagent le même code de hachage.

Renvoyer 0 ou -1 pour null, tant que vous en choisissez un et que vous le retournez tout le temps, fonctionnera. De toute évidence, les codes de hachage non nuls ne doivent pas renvoyer la valeur que vous utilisez pour null.

Questions similaires:

GetHashCode sur les champs nuls?

Que doit retourner GetHashCode lorsque l'identifiant de l'objet est nul?

Les «remarques» de cette entrée MSDN vont plus en détail autour du code de hachage. De manière poignante, la documentation ne fournit aucune couverture ou discussion des valeurs nulles du tout - même pas dans le contenu de la communauté.

Pour résoudre votre problème avec l'énumération, ré-implémentez le code de hachage pour renvoyer une valeur différente de zéro, ajoutez une entrée d'énumération "inconnue" par défaut équivalente à null, ou n'utilisez simplement pas d'énumérations nullables.

Une trouvaille intéressante, au fait.

Un autre problème que je vois généralement avec cela est que le code de hachage ne peut pas représenter un type de 4 octets ou plus qui est nullable sans au moins une collision (d'autant plus que la taille du type augmente). Par exemple, le code de hachage d'un int est juste le int, il utilise donc la plage int complète. Quelle valeur dans cette plage choisissez-vous pour null? Celui que vous choisissez entrera en conflit avec le code de hachage de la valeur lui-même.

Les collisions en elles-mêmes ne sont pas nécessairement un problème, mais vous devez savoir qu'elles sont là. Les codes de hachage ne sont utilisés que dans certaines circonstances. Comme indiqué dans la documentation sur MSDN, les codes de hachage ne sont pas garantis pour renvoyer des valeurs différentes pour différents objets et ne devraient donc pas être attendus.

Adam Houldsworth
la source
Je ne pense pas que les questions que vous liez soient complètement similaires. Lorsque vous surchargez Object.GetHashCode()dans votre propre classe (ou structure), vous savez que ce code ne sera frappé que lorsque les gens ont réellement une instance de votre classe. Cette instance ne peut pas être null. C'est pourquoi vous ne commencez pas votre remplacement de Object.GetHashCode()par if (this == null) return -1;Il y a une différence entre «être null» et «être un objet possédant des champs qui sont null».
Jeppe Stig Nielsen
Vous dites: de toute évidence, les codes de hachage non nuls ne doivent pas retourner la valeur que vous utilisez pour null. Ce serait idéal, je suis d'accord. Et c'est la raison pour laquelle j'ai posé ma question en premier lieu, car chaque fois que nous écrivons une énumération T, alors (T?)nullet (T?)default(T)aura le même code de hachage (dans l'implémentation actuelle de .NET). Cela pourrait être changé si les implémenteurs de .NET modifiaient le code de hachage null ou l'algorithme de code de hachage du System.Enum.
Jeppe Stig Nielsen
Je suis d'accord que les liens étaient pour des champs internes nuls. Vous mentionnez que c'est pour IEqualityComparer <T>, dans votre implémentation le code de hachage est toujours spécifique à un type donc vous êtes toujours dans la même situation, cohérence pour le type. Renvoyer le même code de hachage pour les valeurs nulles de tout type n'a pas d'importance car les valeurs nulles n'ont pas de type.
Adam Houldsworth
1
Remarque: j'ai mis à jour ma question deux fois. Il s'avère que (du moins avec HashSet<>) cela ne fonctionne pas pour changer le code de hachage de null.
Jeppe Stig Nielsen
6

Gardez à l'esprit que le code de hachage est utilisé comme première étape dans la détermination de l'égalité uniquement, et [n'est / ne devrait] jamais (être) utilisé comme une détermination de facto pour savoir si deux objets sont égaux.

Si les codes de hachage de deux objets ne sont pas égaux, alors ils sont traités comme non égaux (car nous supposons que l'implémentation sous-jacente est correcte - c'est-à-dire que nous ne le devinons pas). S'ils ont le même code de hachage, ils doivent alors être vérifiés pour l' égalité réelle qui, dans votre cas, la nullvaleur et la valeur enum échoueront.

En conséquence, l'utilisation de zéro est aussi bonne que toute autre valeur dans le cas général.

Bien sûr, il y aura des situations, comme votre énumération, où ce zéro est partagé avec le code de hachage d'une valeur réelle . La question est de savoir si, pour vous, la minuscule surcharge d'une comparaison supplémentaire pose des problèmes.

Si tel est le cas, définissez votre propre comparateur pour le cas du nullable pour votre type particulier, et assurez-vous qu'une valeur nulle donne toujours un code de hachage qui est toujours le même (bien sûr!) Et une valeur qui ne peut pas être fournie par le sous-jacent. propre algorithme de code de hachage de type. Pour vos propres types, c'est faisable. Pour les autres - bonne chance :)

Andras Zoltan
la source
5

Il n'est pas nécessaire que ce soit zéro - vous pouvez en faire 42 si vous le souhaitez.

Tout ce qui compte, c'est la cohérence lors de l'exécution du programme.

C'est juste la représentation la plus évidente, car elle nullest souvent représentée par un zéro en interne. Ce qui signifie, pendant le débogage, si vous voyez un code de hachage de zéro, cela pourrait vous inciter à penser, "Hmm ... était-ce un problème de référence nul?"

Notez que si vous utilisez un nombre comme 0xDEADBEEF, alors quelqu'un pourrait dire que vous utilisez un nombre magique ... et vous le seriez en quelque sorte. (Vous pourriez dire que zéro est un nombre magique aussi, et vous auriez raison ... sauf qu'il est si largement utilisé qu'il est en quelque sorte une exception à la règle.)

user541686
la source
4

Bonne question.

J'ai juste essayé de coder ceci:

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

et exécutez ceci comme ceci:

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

il retourne null

si je le fais, plutôt normal

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

il revient 0, comme prévu, ou simple Spring si on évite de lancer vers int.

Donc ... si vous faites ce qui suit:

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

ÉDITER

Depuis MSDN

Si deux objets se comparent comme égaux, la méthode GetHashCode de chaque objet doit renvoyer la même valeur. Toutefois, si deux objets ne se comparent pas comme égaux, les méthodes GetHashCode pour les deux objets ne doivent pas renvoyer des valeurs différentes

En d'autres termes: si deux objets ont le même code de hachage qui ne signifie pas qu'ils sont égaux, la cause de l' égalité réelle est déterminée par Equals .

De nouveau à partir de MSDN:

La méthode GetHashCode pour un objet doit systématiquement renvoyer le même code de hachage tant qu'il n'y a aucune modification de l'état de l'objet qui détermine la valeur de retour de la méthode Equals de l'objet. Notez que cela n'est vrai que pour l'exécution en cours d'une application et qu'un code de hachage différent peut être renvoyé si l'application est réexécutée.

Tigran
la source
6
une collision, par définition, signifie que deux objets inégaux ont le même hashcode. Vous avez démontré que les objets ne sont pas égaux. Maintenant, ont-ils le même code de hachage? Selon l'OP, ils le font, ce qui signifie qu'il s'agit d'une collision. Maintenant, ce n'est pas la fin du monde d'avoir une collision, c'est simplement une collision plus probable que si null haché à autre chose que 0, ce qui nuit aux performances.
Servy
1
Alors, que dit votre réponse? Vous dites que Season.Spring n'est pas égal à null. Eh bien, ce n'est pas faux, mais cela ne répond pas vraiment à la question en aucune façon le fait maintenant.
Servy
2
@Servy: la question dit: c'est pourquoi j'ai le même hascode pour 2 objets différents ( null et Spring ). Donc, la réponse est qu'il n'y a pas de cause de collision même avec le même hashcode, ils ne sont pas égaux, d'ailleurs.
Tigran
3
"Réponse: pourquoi pas?" Eh bien, le PO a répondu de manière préventive à votre question «pourquoi pas». Il est plus susceptible de provoquer des collisions qu'un autre nombre. Il se demandait s'il y avait une raison pour laquelle 0 avait été choisi, et personne n'a encore répondu à cela.
Servy
1
Cette réponse ne contient rien que le PO ne sache déjà, ce qui ressort de la manière dont la question a été posée.
Konrad Rudolph
4

Mais y a-t-il une raison pour laquelle le code de hachage de null devrait être 0?

Cela aurait pu être n'importe quoi. J'ai tendance à convenir que 0 n'était pas nécessairement le meilleur choix, mais c'est celui qui conduit probablement au moins de bogues.

Une fonction de hachage doit absolument renvoyer le même hachage pour la même valeur. Une fois qu'il existe un composant qui fait cela, c'est vraiment la seule valeur valide pour le hachage de null. S'il y avait une constante pour cela, comme, hm object.HashOfNull, alors quelqu'un implémentant unIEqualityComparer devrait savoir comment utiliser cette valeur. S'ils n'y pensent pas, la chance qu'ils utilisent 0 est légèrement plus élevée que toutes les autres valeurs, je pense.

au moins pour HashSet <>, il n'est même pas possible de changer le hachage de null

Comme mentionné ci-dessus, je pense que c'est complètement impossible, tout simplement parce qu'il existe des types qui suivent déjà la convention selon laquelle le hachage de null est 0.

Roman Starkov
la source
Quand on implémente la méthode EqualityComparer<T>.GetHashCode(T)pour un type particulier Tqui le permet null, on doit faire quelque chose quand l'argument est null. Vous pouvez (1) lancer un ArgumentNullException, (2) retourner 0ou (3) retourner quelque chose d'autre. Je prends ta réponse pour une recommandation de toujours revenir 0dans cette situation?
Jeppe Stig Nielsen
@JeppeStigNielsen Je ne suis pas sûr de lancer contre retour, mais si vous choisissez de revenir, alors certainement zéro.
Roman Starkov
2

C'est 0 par souci de simplicité. Il n'y a pas d'exigence aussi stricte. Il vous suffit de vous assurer des exigences générales du codage de hachage.

Par exemple, vous devez vous assurer que si deux objets sont égaux, leurs hashcodes doivent toujours être égaux également. Par conséquent, différents codes de hachage doivent toujours représenter des objets différents (mais ce n'est pas nécessairement vrai vice versa: deux objets différents peuvent avoir le même code de hachage, même si cela se produit souvent, ce n'est pas une fonction de hachage de bonne qualité - elle n'a pas bonne résistance aux collisions).

Bien sûr, j'ai limité ma réponse aux exigences de nature mathématique. Il existe également des conditions techniques spécifiques à .NET, que vous pouvez lire ici . 0 pour une valeur nulle n'en fait pas partie.

Thomas Calc
la source
1

Cela pourrait donc être évité en utilisant une Unknownvaleur enum (même si cela semble un peu étrange pour a Seasond'être inconnu). Donc, quelque chose comme ça annulerait ce problème:

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

Ensuite, vous auriez des valeurs de code de hachage uniques pour chaque saison.

SwDevMan81
la source
1
oui, mais cela ne répond pas réellement à la question. De cette manière, la question null se heurtera à Uknown. Qu'est-ce qu'une différence?
Tigran
@Tigran - Cette version n'utilise pas de type Nullable
SwDevMan81
Je vois, mais la question concerne le type Nullable.
Tigran
J'ai une scène un million de fois sur SO que les gens proposent des suggestions d'amélioration comme réponses.
SwDevMan81
1

Personnellement, je trouve que l'utilisation de valeurs nullables est un peu gênante et j'essaie de les éviter chaque fois que je le peux. Votre problème n'est qu'une autre raison. Parfois, ils sont très pratiques, mais ma règle de base est de ne pas mélanger les types valeur avec null si possible simplement parce qu'ils proviennent de deux mondes différents. Dans le framework .NET, ils semblent faire la même chose - de nombreux types de valeur fournissent une TryParseméthode qui permet de séparer les valeurs de l'absence de valeur (null ).

Dans votre cas particulier, il est facile de se débarrasser du problème car vous gérez votre propre Seasontype.

(Season?)nullpour moi signifie «la saison n'est pas spécifiée», comme lorsque vous avez un formulaire Web où certains champs ne sont pas obligatoires. À mon avis, il est préférable de spécifier cette «valeur» spéciale en enumsoi plutôt que d'utiliser un peu maladroit Nullable<T>. Il sera plus rapide (pas de boxe) plus facile à lire ( Season.NotSpecifiedvs null) et résoudra votre problème avec les codes de hachage.

Bien sûr pour d'autres types, comme intvous ne pouvez pas étendre le domaine de valeur et désigner l'une des valeurs comme spéciale n'est pas toujours possible. Mais avec le int?code de hachage, la collision est un problème beaucoup plus petit, voire pas du tout.

Maciej
la source
Quand vous dites "boxing", je pense que vous voulez dire "wrapping", c'est-à-dire mettre une valeur de struct dans un Nullable<>struct (où le HasValuemembre sera alors défini true). Êtes-vous sûr que le problème est vraiment plus petit avec int?? La plupart du temps, on n'utilise que quelques valeurs de int, puis cela équivaut à une énumération (qui peut en théorie avoir plusieurs membres).
Jeppe Stig Nielsen
En général, je dirais que enum est choisi quand il y a un nombre limité de valeurs connues requises (2-10). Si la limite est plus grande ou nulle, intcela a plus de sens. Bien sûr, les préférences varient.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Denis535
la source
1
C'est une approche intéressante. Il serait utile de modifier votre réponse pour inclure des explications supplémentaires, et surtout compte tenu de la nature de la question.
Jeremy Caney