Étant donné que des collections comme System.Collections.Generic.HashSet<>
accept null
comme un membre d'ensemble, on peut se demander quel null
devrait ê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 null
et 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 null
devrait ê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 ArgumentNullException
si 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 null
objet (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.Spring
et null
se termineront dans le même compartiment. Cela peut être déterminé en inspectant très soigneusement les membres du tableau privé m_buckets
et 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;
}
}