Über die Bedeutung von GetHashCode
Andere haben bereits kommentiert, dass jede benutzerdefinierte IEqualityComparer<T>
Implementierung wirklich eine GetHashCode
Methode 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 Distinct
zum Beispiel. Berücksichtigen Sie die Auswirkungen dieser Erweiterungsmethode, wenn nur eine Equals
Methode 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 Distinct
ungünstigsten Fall verwendet wird !
Zum Glück ist dies nicht der Fall. Distinct
nicht nur verwenden Equals
; es verwendet GetHashCode
auch. 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 Distinct
Verwendung 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 Value
Elementen mit derselben Name
Eigenschaft 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 GroupBy
zu 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, GetHashCode
in 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 keyExtractor
selbst.
- Zweitens würde ich tatsächlich eine
where TKey : IEquatable<TKey>
Einschränkung hinzufügen . Dies verhindert das Boxen im Equals
Aufruf ( object.Equals
nimmt einen object
Parameter an; Sie benötigen eine IEquatable<TKey>
Implementierung, um einen TKey
Parameter 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.