O znaczeniu GetHashCode
Inni już skomentowali fakt, że każda niestandardowa IEqualityComparer<T>
implementacja powinna naprawdę zawierać GetHashCode
metodę ; ale nikt nie zadał sobie trudu, aby szczegółowo wyjaśnić dlaczego .
Dlatego. Twoje pytanie konkretnie wspomina o metodach rozszerzających LINQ; Prawie wszystkie z nich polegają na kodach skrótu, aby działać poprawnie, ponieważ wewnętrznie wykorzystują tabele skrótów w celu zwiększenia wydajności.
Weźmy Distinct
na przykład. Rozważ konsekwencje tej metody rozszerzającej, jeśli wszystko, co wykorzystała, było Equals
metodą. Jak ustalisz, czy element został już zeskanowany w sekwencji, jeśli tylko to zrobiłeś Equals
? Wyliczasz wszystkie wartości, które już przeglądałeś, i sprawdzasz, czy są zgodne. Spowodowałoby to Distinct
użycie algorytmu O (N 2 ) w najgorszym przypadku zamiast algorytmu O (N)!
Na szczęście tak nie jest. Distinct
nie tylko używa Equals
; używa GetHashCode
również. W rzeczywistości absolutnie nie działa poprawnie bez IEqualityComparer<T>
dostarczenia właściwegoGetHashCode
. Poniżej znajduje się wymyślony przykład ilustrujący to.
Powiedzmy, że mam następujący typ:
class Value
{
public string Name { get; private set; }
public int Number { get; private set; }
public Value(string name, int number)
{
Name = name;
Number = number;
}
public override string ToString()
{
return string.Format("{0}: {1}", Name, Number);
}
}
Teraz powiedz, że mam List<Value>
i chcę znaleźć wszystkie elementy o różnych nazwach. Jest to doskonały przypadek Distinct
użycia niestandardowego modułu porównującego równość. Skorzystajmy więc z Comparer<T>
klasy z odpowiedzi Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Teraz, jeśli mamy kilka Value
elementów o tej samej Name
właściwości, wszystkie powinny zwinąć się w jedną wartość zwróconą przez Distinct
, prawda? Zobaczmy...
var values = new List<Value>();
var random = new Random();
for (int i = 0; i < 10; ++i)
{
values.Add("x", random.Next());
}
var distinct = values.Distinct(comparer);
foreach (Value x in distinct)
{
Console.WriteLine(x);
}
Wynik:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, to nie zadziałało, prawda?
O co chodzi GroupBy
? Spróbujmy tego:
var grouped = values.GroupBy(x => x, comparer);
foreach (IGrouping<Value> g in grouped)
{
Console.WriteLine("[KEY: '{0}']", g);
foreach (Value x in g)
{
Console.WriteLine(x);
}
}
Wynik:
[KEY = 'x: 1346013431']
x: 1346013431
[KEY = 'x: 1388845717']
x: 1388845717
[KEY = 'x: 1576754134']
x: 1576754134
[KEY = 'x: 1104067189']
x: 1104067189
[KEY = 'x: 1144789201']
x: 1144789201
[KEY = 'x: 1862076501']
x: 1862076501
[KEY = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377
Ponownie: nie zadziałało.
Jeśli się nad tym zastanowić, sensowne Distinct
byłoby użycie a HashSet<T>
(lub odpowiednika) wewnętrznie i GroupBy
użycie czegoś takiego jak Dictionary<TKey, List<T>>
wewnętrznie. Czy to mogłoby wyjaśnić, dlaczego te metody nie działają? Spróbujmy tego:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Wynik:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Tak ... zaczyna to mieć sens?
Mamy nadzieję, że z tych przykładów jasno wynika, dlaczego uwzględnienie odpowiedniego rozwiązania GetHashCode
w każdej IEqualityComparer<T>
implementacji jest tak ważne.
Oryginalna odpowiedź
Rozszerzanie odpowiedzi Oripa :
Można tu wprowadzić kilka ulepszeń.
- Po pierwsze, wziąłbym
Func<T, TKey>
zamiast Func<T, object>
; Zapobiegnie to umieszczaniu kluczy typu wartości w keyExtractor
samej rzeczywistości .
- Po drugie, właściwie dodałbym
where TKey : IEquatable<TKey>
ograniczenie; zapobiegnie to opakowaniu w Equals
wywołaniu ( object.Equals
pobiera object
parametr; potrzebujesz IEquatable<TKey>
implementacji, aby pobrać TKey
parametr bez pakowania go w ramki). Oczywiście może to stanowić zbyt poważne ograniczenie, więc można utworzyć klasę bazową bez ograniczenia, a wraz z nią klasę pochodną.
Oto, jak może wyglądać wynikowy kod:
public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
protected readonly Func<T, TKey> keyExtractor;
public KeyEqualityComparer(Func<T, TKey> keyExtractor)
{
this.keyExtractor = keyExtractor;
}
public virtual bool Equals(T x, T y)
{
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
public int GetHashCode(T obj)
{
return this.keyExtractor(obj).GetHashCode();
}
}
public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
where TKey : IEquatable<TKey>
{
public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
: base(keyExtractor)
{ }
public override bool Equals(T x, T y)
{
// This will use the overload that accepts a TKey parameter
// instead of an object parameter.
return this.keyExtractor(x).Equals(this.keyExtractor(y));
}
}
IEqualityComparer<T>
co pomijaGetHashCode
, jest po prostu złamane.