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;
}
}
Odpowiedzi:
Tak długo, jak kod skrótu zwracany dla wartości null jest spójny dla typu, wszystko powinno być w porządku. Jedynym wymaganiem dla kodu skrótu jest to, że dwa obiekty, które są uważane za równe, mają ten sam kod skrótu.
Zwracanie 0 lub -1 dla null, o ile wybierzesz jeden i będziesz go zwracać przez cały czas, zadziała. Oczywiście kody skrótu inne niż null nie powinny zwracać żadnej wartości, której używasz dla null.
Podobne pytania:GetHashCode na pustych polach?
Co powinien zwrócić GetHashCode, gdy identyfikator obiektu ma wartość null?
W sekcji „Uwagi” tego wpisu MSDN opisano bardziej szczegółowo kod skrótu. Przejmująco, dokumentacja nie zawiera żadnego pokrycia lub omówienie wartości null w ogóle - nawet w zawartość.Aby rozwiązać problem z wyliczeniem, zaimplementuj ponownie kod skrótu, aby zwracał wartość różną od zera, dodaj domyślny wpis wyliczenia „nieznany” równoważny null lub po prostu nie używaj wyliczeń dopuszczających wartość null.
Nawiasem mówiąc, ciekawe znalezisko.
Innym problemem, który widzę z tym ogólnie, jest to, że kod skrótu nie może reprezentować 4-bajtowego lub większego typu, który jest dopuszczalny bez co najmniej jednej kolizji (więcej, gdy zwiększa się rozmiar typu). Na przykład kod skrótu int to po prostu int, więc używa pełnego zakresu int. Jaką wartość w tym zakresie wybierasz dla null? Cokolwiek wybierzesz, zderzy się z samym kodem skrótu wartości.
Zderzenia same w sobie niekoniecznie są problemem, ale musisz wiedzieć, że one istnieją. Kody skrótu są używane tylko w niektórych okolicznościach. Jak stwierdzono w dokumentacji na MSDN, kody skrótów nie gwarantują zwrócenia różnych wartości dla różnych obiektów, więc nie należy się tego spodziewać.
źródło
Object.GetHashCode()
w swojej własnej klasie (lub strukturze), wiesz, że ten kod zostanie trafiony tylko wtedy, gdy ludzie faktycznie mają instancję Twojej klasy. Taka instancja nie może byćnull
. To dlaczego nie zacząć od overrideObject.GetHashCode()
zif (this == null) return -1;
Istnieje różnica między „byćnull
” i „bycia obiektem posiadającym kilka pól, które sąnull
”.T
, wtedy(T?)null
i(T?)default(T)
będziemy mieć ten sam kod skrótu (w obecnej implementacji .NET). Można to zmienić, jeśli implementatorzy platformy .NET zmienili kod skrótunull
lub algorytm kodu skrótuSystem.Enum
.HashSet<>
) nie działa zmiana kodu skrótu programunull
.Należy pamiętać, że kod skrótu jest używany jako pierwszy krok tylko do określania równości i [jest / nie powinien] nigdy (być) używany jako faktyczne określenie, czy dwa obiekty są równe.
Jeśli kody skrótu dwóch obiektów nie są równe, to są traktowane jako nierówne (ponieważ zakładamy, że podstawowa implementacja jest poprawna - tj. Nie odgadujemy tego ponownie). Jeśli mają ten sam kod skrótu, należy je następnie sprawdzić pod kątem rzeczywistej równości, co w twoim przypadku
null
nie powiedzie się wartość i wartość wyliczenia.W rezultacie - użycie zera jest tak samo dobre jak każdej innej wartości w ogólnym przypadku.
Jasne, będą sytuacje, takie jak wyliczenie, w których to zero jest współdzielone z kodem skrótu rzeczywistej wartości. Pytanie brzmi, czy malutki koszt dodatkowego porównania powoduje problemy.
Jeśli tak, zdefiniuj własną funkcję porównującą dla przypadku wartości null dla określonego typu i upewnij się, że wartość null zawsze daje kod skrótu, który jest zawsze taki sam (oczywiście!) I wartość, której nie może uzyskać podstawowa własny algorytm kodu skrótu. Dla twoich własnych typów jest to możliwe. Dla innych - powodzenia :)
źródło
Nie musi to być zero - jeśli chcesz, możesz zrobić to 42.
Liczy się tylko konsekwencja w realizacji programu.
To najbardziej oczywista reprezentacja, ponieważ
null
często jest reprezentowana jako zero wewnętrznie. Co oznacza, że podczas debugowania, jeśli zobaczysz kod skrótu o wartości zero, możesz pomyśleć: „Hmm… czy to był problem z zerową referencją?”Zwróć uwagę, że jeśli użyjesz liczby takiej jak
0xDEADBEEF
, to ktoś może powiedzieć, że używasz magicznej liczby ... i tak jakbyś był. (Można powiedzieć, że zero jest również magiczną liczbą i miałbyś rację ... z wyjątkiem tego, że jest tak szeroko stosowany, że jest czymś w rodzaju wyjątku od reguły.)źródło
Dobre pytanie.
Właśnie próbowałem to zakodować:
enum Season { Spring, Summer, Autumn, Winter, }
i wykonaj to w ten sposób:
Season? v = null; Console.WriteLine(v);
wraca
null
jeśli tak, zamiast normalne
Season? v = Season.Spring; Console.WriteLine((int)v);
powróci
0
, zgodnie z oczekiwaniami, lub po prostu Spring, jeśli unikniemy rzucaniaint
.Więc .. jeśli wykonasz następujące czynności:
Season? v = Season.Spring; Season? vnull = null; if(vnull == v) // never TRUE
EDYTOWAĆ
Z MSDN
Jeśli dwa obiekty są porównywane jako równe, metoda GetHashCode dla każdego obiektu musi zwracać tę samą wartość. Jeśli jednak dwa obiekty nie są porównywane jako równe, metody GetHashCode dla tych dwóch obiektów nie muszą zwracać różnych wartości
Innymi słowy: jeśli dwa obiekty mają ten sam kod skrótu, co nie oznacza, że są równe, to rzeczywista równość jest określana przez Equals .
Z MSDN ponownie:
źródło
To mogło być cokolwiek. Zwykle się zgadzam, że 0 niekoniecznie było najlepszym wyborem, ale prawdopodobnie prowadzi do najmniejszej liczby błędów.
Funkcja skrótu bezwzględnie musi zwracać ten sam skrót dla tej samej wartości. Raz istnieje na składnik, który to robi, to jest naprawdę ważne tylko wartość dla skrótu
null
. Gdyby istniała dla tego stała, taka jak hmobject.HashOfNull
, to ktoś wdrażający aIEqualityComparer
musiałby wiedzieć, jak użyć tej wartości. Jeśli o tym nie pomyślą, szansa, że użyją 0, jest nieco wyższa niż każda inna wartość, jak sądzę.Jak wspomniano powyżej, myślę, że jest to całkowicie niemożliwe, ponieważ istnieją typy, które już są zgodne z konwencją, w której hash z null wynosi 0.
źródło
EqualityComparer<T>.GetHashCode(T)
dla jakiegoś określonego typu,T
który na to pozwalanull
, trzeba coś zrobić, gdy argument jestnull
. Możesz (1) rzucićArgumentNullException
, (2) zwrócić0
lub (3) zwrócić coś innego. Przyjmuję twoją odpowiedź za zalecenie, aby zawsze wracać0
w takiej sytuacji?Ze względu na prostotę jest to 0. Nie ma takiego surowego wymogu. Musisz tylko zapewnić ogólne wymagania dotyczące kodowania hash.
Na przykład musisz się upewnić, że jeśli dwa obiekty są równe, ich hashcodes również muszą być równe. Dlatego różne hashcody muszą zawsze reprezentować różne obiekty (ale niekoniecznie jest to prawdą odwrotnie: dwa różne obiekty mogą mieć ten sam hashcode, nawet jeśli zdarza się to często, nie jest to dobrej jakości funkcja hashująca - nie ma dobra odporność na kolizje).
Oczywiście ograniczyłem swoją odpowiedź do wymagań natury matematycznej. Istnieją również warunki techniczne specyficzne dla platformy .NET, o których można przeczytać tutaj . 0 dla wartości zerowej nie ma wśród nich.
źródło
Można więc tego uniknąć, używając
Unknown
wartości wyliczenia (chociaż wydaje się nieco dziwne, że aSeason
jest nieznane). Więc coś takiego mogłoby zanegować ten problem: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
Wtedy miałbyś unikalne wartości kodu skrótu dla każdego sezonu.
źródło
Osobiście uważam, że używanie wartości zerowych jest trochę niezręczne i staram się ich unikać, kiedy tylko mogę. Twój problem to tylko kolejny powód. Czasami są jednak bardzo przydatne, ale moją zasadą jest, aby nie mieszać typów wartości z null, jeśli to możliwe, tylko dlatego, że pochodzą z dwóch różnych światów. W środowisku .NET wydają się robić to samo - wiele typów wartości udostępnia
TryParse
metodę, która jest sposobem na oddzielenie wartości od żadnych wartości (null
).W twoim konkretnym przypadku łatwo jest pozbyć się problemu, ponieważ radzisz sobie z własnym
Season
typem.(Season?)null
dla mnie oznacza „sezon nie jest określony”, tak jak w przypadku formularza internetowego, w którym niektóre pola nie są wymagane. Moim zdaniem lepiej jest określić tę specjalną „wartość” wenum
sobie, zamiast używać jej trochę niezgrabnieNullable<T>
. Będzie szybszy (bez boksu), łatwiejszy do odczytania (wSeason.NotSpecified
porównaniu znull
) i rozwiąże Twój problem z kodami skrótu.Oczywiście w przypadku innych typów, na przykład
int
nie można rozszerzyć dziedziny wartości, a określenie jednej z wartości jako specjalnej nie zawsze jest możliwe. Aleint?
kolizja kodu skrótu jest znacznie mniejszym problemem, jeśli w ogóle.źródło
Nullable<>
struktury (gdzie elementHasValue
członkowski zostanie ustawiony natrue
). Czy na pewno problem jest naprawdę mniejszyint?
? W wielu przypadkach używa się tylko kilku wartościint
, a następnie jest to równoważne wyliczeniu (które teoretycznie może mieć wielu członków).int
ma go wcale, ma to większy sens. Oczywiście preferencje są różne.Tuple.Create( (object) null! ).GetHashCode() // 0 Tuple.Create( 0 ).GetHashCode() // 0 Tuple.Create( 1 ).GetHashCode() // 1 Tuple.Create( 2 ).GetHashCode() // 2
źródło