Biorąc pod uwagę, że kolekcje takie jak System.Collections.Generic.HashSet<>
accept null
jako członek zestawu, można zapytać, jaki null
powinien być kod skrótu . Wygląda na to, że framework używa 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
Może to być (trochę) problematyczne w przypadku wyliczeń dopuszczających wartość null. Jeśli zdefiniujemy
enum Season
{
Spring,
Summer,
Autumn,
Winter,
}
wtedy Nullable<Season>
(również nazywany Season?
) może przyjąć tylko pięć wartości, ale dwie z nich, mianowicie null
i Season.Spring
, mają ten sam kod skrótu.
Kuszące byłoby napisanie „lepszego” narzędzia do porównywania równości w następujący sposób:
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;
}
}
Ale czy jest jakiś powód, dla którego null
powinien być kod skrótu 0
?
EDYCJA / DODANIE:
Niektórym wydaje się, że chodzi o nadpisywanie Object.GetHashCode()
. Tak naprawdę nie jest. (Jednak autorzy .NET dokonali nadpisania GetHashCode()
w Nullable<>
strukturze, co jest istotne). Napisana przez użytkownika implementacja parametru GetHashCode()
bezparametrowego nigdy nie poradzi sobie z sytuacją, w której znajduje się obiekt, którego szukamy kodu skrótu null
.
Chodzi o implementację metody abstrakcyjnej EqualityComparer<T>.GetHashCode(T)
lub inną implementację metody interfejsu IEqualityComparer<T>.GetHashCode(T)
. Teraz, podczas tworzenia tych linków do MSDN, widzę, że jest tam napisane, że te metody rzucają, ArgumentNullException
jeśli ich jedynym argumentem jest null
. To z pewnością błąd w MSDN? Żadna z własnych implementacji platformy .NET nie zgłasza wyjątków. Rzucenie w takim przypadku skutecznie przerwałoby każdą próbę dodania null
do HashSet<>
. Chyba że HashSet<>
robi coś niezwykłego, gdy ma do czynienia z null
przedmiotem (będę musiał to przetestować).
NOWA EDYCJA / DODATEK:
Teraz próbowałem debugować. Dzięki HashSet<>
, mogę potwierdzić, że z comparer domyślny równości, wartości Season.Spring
i null
będzie kończyć się w tym samym segmencie. Można to ustalić, bardzo dokładnie sprawdzając prywatne elementy tablicy m_buckets
i m_slots
. Zauważ, że indeksy są zawsze, zgodnie z projektem, przesunięte o jeden.
Kod, który podałem powyżej, jednak tego nie naprawia. Jak się okazuje, HashSet<>
nigdy nawet nie zapyta modułu porównującego równość, kiedy wartość wynosi null
. To pochodzi z kodu źródłowego 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;
}
Oznacza to, że przynajmniej w przypadku HashSet<>
nie można nawet zmienić skrótu pliku null
. Zamiast tego rozwiązaniem jest zmiana skrótu wszystkich innych wartości, na przykład:
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;
}
}