Sull'importanza di GetHashCode
Altri hanno già commentato il fatto che qualsiasi IEqualityComparer<T>
implementazione personalizzata dovrebbe davvero includere un GetHashCode
metodo ; ma nessuno si è preso la briga di spiegare il perché in alcun dettaglio.
Ecco perché. La tua domanda menziona specificamente i metodi di estensione LINQ; quasi tutti questi si basano su codici hash per funzionare correttamente, poiché utilizzano le tabelle hash internamente per l'efficienza.
Prendi Distinct
, per esempio. Considera le implicazioni di questo metodo di estensione se tutto ciò che utilizzava fosse un Equals
metodo. Come si determina se un articolo è già stato scansionato in sequenza se solo lo si è Equals
? Enumeri sull'intera raccolta di valori che hai già visto e controlli una corrispondenza. Ciò comporterebbe l' Distinct
uso di un algoritmo O (N 2 ) nel caso peggiore invece di uno O (N)!
Fortunatamente, non è così. Distinct
non usa soloEquals
; usa GetHashCode
anche. In realtà, non funziona assolutamente correttamente senza IEqualityComparer<T>
quello che fornisce un veroGetHashCode
. Di seguito è riportato un esempio inventato che illustra questo.
Di 'che ho il seguente tipo:
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);
}
}
Ora dì che ho un List<Value>
e voglio trovare tutti gli elementi con un nome distinto. Questo è un caso d'uso perfetto per l' Distinct
utilizzo di un comparatore di uguaglianza personalizzato. Quindi usiamo la Comparer<T>
classe dalla risposta di Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Ora, se abbiamo un gruppo di Value
elementi con la stessa Name
proprietà, dovrebbero tutti collassare in un valore restituito da Distinct
, giusto? Vediamo...
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);
}
Produzione:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, non ha funzionato, vero?
Che dire GroupBy
? Proviamo che:
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);
}
}
Produzione:
[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
Ancora: non ha funzionato.
Se ci pensate, avrebbe senso Distinct
usare un HashSet<T>
(o equivalente) internamente e GroupBy
usare qualcosa come un Dictionary<TKey, List<T>>
internamente. Questo potrebbe spiegare perché questi metodi non funzionano? Proviamo questo:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Produzione:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Sì ... cominciando a dare un senso?
Speriamo che da questi esempi sia chiaro perché includere un appropriato GetHashCode
in qualsiasi IEqualityComparer<T>
implementazione sia così importante.
Risposta originale
Espandendo sulla risposta di orip :
Ci sono un paio di miglioramenti che possono essere fatti qui.
- Innanzitutto, prenderei
Func<T, TKey>
invece di Func<T, object>
; questo impedirà l'inscatolamento delle chiavi del tipo di valore nell'effettivo keyExtractor
stesso.
- In secondo luogo, aggiungerei effettivamente un
where TKey : IEquatable<TKey>
vincolo; questo eviterà l'inscatolamento nella Equals
chiamata ( object.Equals
accetta un object
parametro; è necessaria IEquatable<TKey>
un'implementazione per accettare un TKey
parametro senza inscatolarlo). Chiaramente ciò può comportare una restrizione troppo grave, quindi è possibile creare una classe base senza il vincolo e una classe derivata con essa.
Ecco come potrebbe apparire il codice risultante:
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>
ciò che lasciaGetHashCode
è semplicemente rotto.