Ponieważ nie mogłem znaleźć odpowiedzi, która wyjaśnia, dlaczego powinniśmy nadpisywać GetHashCode
i Equals
dla struktur niestandardowych oraz dlaczego domyślna implementacja „prawdopodobnie nie będzie odpowiednia do użycia jako klucz w tablicy skrótów”, zostawię link do tego bloga post , który wyjaśnia, dlaczego na przykładzie rzeczywistego problemu, który się wydarzył.
Polecam przeczytanie całego posta, ale tutaj jest podsumowanie (podkreślenie i dodane wyjaśnienia).
Powód, dla którego domyślny skrót dla struktur jest powolny i niezbyt dobry:
Sposób zaprojektowania CLR, każde wywołanie członka zdefiniowanego w System.ValueType
lub System.Enum
typach [może] spowodować alokację boksów [...]
Osoba realizująca funkcję skrótu stoi przed dylematem: dokonać dobrej dystrybucji funkcji skrótu lub przyspieszyć. W niektórych przypadkach możliwe jest osiągnięcie obu, ale jest to trudne do zrobienia ogólnie w ValueType.GetHashCode
.
Kanoniczna funkcja skrótu struktury „łączy” kody skrótów wszystkich pól. Ale jedynym sposobem uzyskania skrótu pola w ValueType
metodzie jest użycie odbicia . Tak więc autorzy CLR zdecydowali się zamienić prędkość na dystrybucję i GetHashCode
wersja domyślna po prostu zwraca kod skrótu pierwszego pola innego niż null i "łączy" go z identyfikatorem typu [...] Jest to rozsądne zachowanie, chyba że tak nie jest . Na przykład, jeśli masz pecha i pierwsze pole twojej struktury ma tę samą wartość dla większości instancji, funkcja skrótu będzie zawsze zapewniać ten sam wynik . I, jak możesz sobie wyobrazić, spowoduje to drastyczny wpływ na wydajność, jeśli te wystąpienia będą przechowywane w zestawie skrótów lub tabeli skrótów.
[...] Wdrażanie oparte na refleksji przebiega powoli . Bardzo wolno.
[…] Obie ValueType.Equals
i ValueType.GetHashCode
mają specjalną optymalizację. Jeśli typ nie ma „wskaźników” i jest odpowiednio spakowany [...], wówczas używane są bardziej optymalne wersje: GetHashCode
iteruje po instancji i blokach XOR o wielkości 4 bajtów, a Equals
metoda porównuje dwie instancje przy użyciu memcmp
. […] Ale optymalizacja jest bardzo trudna. Po pierwsze, trudno jest stwierdzić, kiedy optymalizacja jest włączona [...] Po drugie, porównanie pamięci niekoniecznie da prawidłowe wyniki . Oto prosty przykład: [...] -0.0
i +0.0
są równe, ale mają różne reprezentacje binarne.
Rzeczywisty problem opisany w poście:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Użyliśmy krotki, która zawierała niestandardową strukturę z domyślną implementacją równości. I niestety, struktura miała opcjonalne pierwsze pole, które prawie zawsze było równe [pusty łańcuch] . Wydajność była OK, dopóki liczba elementów w zestawie nie wzrosła znacząco, powodując rzeczywisty problem z wydajnością, a zainicjowanie kolekcji z dziesiątkami tysięcy elementów zajmowało minuty.
Tak więc, aby odpowiedzieć na pytanie „w jakich przypadkach powinienem spakować swoją własną iw jakich przypadkach mogę bezpiecznie polegać na domyślnej implementacji”, przynajmniej w przypadku struktur , należy nadpisać Equals
i GetHashCode
zawsze, gdy niestandardowa struktura może być używana jako klucz w tablicy skrótów lub Dictionary
.
Poleciłbym również wdrożenie IEquatable<T>
w tym przypadku, aby uniknąć boksu.
Jak powiedziały inne odpowiedzi, jeśli piszesz klasę , domyślny skrót używający równości odwołań jest zwykle w porządku, więc nie zawracałbym sobie w tym przypadku, chyba że musisz nadpisać Equals
(wtedy musiałbyś odpowiednio nadpisać GetHashCode
).