Sull'importanza di GetHashCode
Altri hanno già commentato il fatto che qualsiasi IEqualityComparer<T>implementazione personalizzata dovrebbe davvero includere un GetHashCodemetodo ; 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 Equalsmetodo. 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' Distinctuso di un algoritmo O (N 2 ) nel caso peggiore invece di uno O (N)!
Fortunatamente, non è così. Distinctnon usa soloEquals ; usa GetHashCodeanche. 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' Distinctutilizzo 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 Valueelementi con la stessa Nameproprietà, 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 Distinctusare un HashSet<T>(o equivalente) internamente e GroupByusare 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 GetHashCodein 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 keyExtractorstesso.
- In secondo luogo, aggiungerei effettivamente un
where TKey : IEquatable<TKey>vincolo; questo eviterà l'inscatolamento nella Equalschiamata ( object.Equalsaccetta un objectparametro; è necessaria IEquatable<TKey>un'implementazione per accettare un TKeyparametro 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.