Über die Bedeutung von GetHashCode
Andere haben bereits kommentiert, dass jede benutzerdefinierte IEqualityComparer<T>Implementierung wirklich eine GetHashCodeMethode enthalten sollte . aber niemand hat sich die Mühe gemacht, im Detail zu erklären, warum .
Hier ist der Grund. In Ihrer Frage werden speziell die LINQ-Erweiterungsmethoden erwähnt. Fast alle von ihnen sind auf Hash-Codes angewiesen, um ordnungsgemäß zu funktionieren, da sie aus Effizienzgründen intern Hash-Tabellen verwenden.
Nehmen wir Distinctzum Beispiel. Berücksichtigen Sie die Auswirkungen dieser Erweiterungsmethode, wenn nur eine EqualsMethode verwendet wurde. Wie stellen Sie fest, ob ein Artikel bereits in einer Sequenz gescannt wurde, wenn Sie nur haben Equals? Sie zählen die gesamte Sammlung von Werten auf, die Sie bereits angesehen haben, und suchen nach Übereinstimmungen. Dies würde dazu führen, dass anstelle eines O (N) -Algorithmus ein O (N 2 ) -Algorithmus im Distinctungünstigsten Fall verwendet wird !
Zum Glück ist dies nicht der Fall. Distinctnicht nur verwenden Equals; es verwendet GetHashCodeauch. In der Tat funktioniert es absolut nicht richtig ohne eine IEqualityComparer<T>, die eine ordnungsgemäße liefertGetHashCode . Unten sehen Sie ein Beispiel, das dies veranschaulicht.
Angenommen, ich habe den folgenden 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);
}
}
Sagen Sie jetzt, ich habe eine List<Value>und ich möchte alle Elemente mit einem eindeutigen Namen finden. Dies ist ein perfekter Anwendungsfall für die DistinctVerwendung eines benutzerdefinierten Gleichheitsvergleichs. Verwenden wir also die Comparer<T>Klasse aus Akus Antwort :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Wenn wir nun eine Reihe von ValueElementen mit derselben NameEigenschaft haben, sollten sie alle zu einem Wert zusammenfallen, der von zurückgegeben wird Distinct, oder? Mal schauen...
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);
}
Ausgabe:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, das hat nicht funktioniert, oder?
Was ist mit GroupBy? Versuchen wir das mal:
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);
}
}
Ausgabe:
[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
Wieder: hat nicht funktioniert.
Wenn Sie darüber nachdenken, wäre es sinnvoll Distinct, ein HashSet<T>(oder ein gleichwertiges) intern GroupByzu verwenden und so etwas wie ein Dictionary<TKey, List<T>>internes zu verwenden. Könnte dies erklären, warum diese Methoden nicht funktionieren? Lass uns das versuchen:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Ausgabe:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Ja ... fängt es an Sinn zu machen?
Aus diesen Beispielen geht hoffentlich klar hervor, warum es so wichtig ist, GetHashCodein jede IEqualityComparer<T>Implementierung ein geeignetes Element aufzunehmen.
Ursprüngliche Antwort
Die Antwort von orip erweitern :
Hier können einige Verbesserungen vorgenommen werden.
- Zuerst würde ich ein
Func<T, TKey>statt nehmen Func<T, object>; Dies verhindert das Boxen von Werttypschlüsseln im tatsächlichen keyExtractorselbst.
- Zweitens würde ich tatsächlich eine
where TKey : IEquatable<TKey>Einschränkung hinzufügen . Dies verhindert das Boxen im EqualsAufruf ( object.Equalsnimmt einen objectParameter an; Sie benötigen eine IEquatable<TKey>Implementierung, um einen TKeyParameter zu nehmen , ohne ihn zu boxen). Dies kann eindeutig eine zu strenge Einschränkung darstellen, sodass Sie eine Basisklasse ohne die Einschränkung und eine abgeleitete Klasse damit erstellen können.
So könnte der resultierende Code aussehen:
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>, was weggelassen wirdGetHashCode, ist einfach kaputt.