Sur l'importance de GetHashCode
D'autres ont déjà commenté le fait que toute IEqualityComparer<T>
implémentation personnalisée devrait vraiment inclure une GetHashCode
méthode ; mais personne n'a pris la peine d'expliquer pourquoi en détail.
Voici pourquoi. Votre question mentionne spécifiquement les méthodes d'extension LINQ; presque tous reposent sur des codes de hachage pour fonctionner correctement, car ils utilisent des tables de hachage en interne pour plus d'efficacité.
Prenez Distinct
, par exemple. Considérez les implications de cette méthode d'extension si tout ce qu'elle utilisait était une Equals
méthode. Comment déterminer si un élément a déjà été numérisé dans une séquence si vous ne l'avez déjà fait Equals
? Vous énumérez l'ensemble de la collection de valeurs que vous avez déjà consultées et recherchez une correspondance. Cela entraînerait l' Distinct
utilisation d'un algorithme O (N 2 ) du pire des cas au lieu d'un algorithme O (N)!
Heureusement, ce n'est pas le cas. Distinct
ne pas simplement utiliser Equals
; il utilise GetHashCode
aussi. En fait, il ne fonctionne absolument pas correctement sans un IEqualityComparer<T>
qui fournit un bonGetHashCode
. Voici un exemple artificiel illustrant cela.
Disons que j'ai le type suivant:
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);
}
}
Maintenant, disons que j'ai un List<Value>
et je veux trouver tous les éléments avec un nom distinct. C'est un cas d'utilisation parfait pour Distinct
utiliser un comparateur d'égalité personnalisé. Alors utilisons la Comparer<T>
classe de la réponse d' Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Maintenant, si nous avons un tas d' Value
éléments avec la même Name
propriété, ils devraient tous se réduire en une seule valeur renvoyée par Distinct
, non? Voyons voir...
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);
}
Production:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, ça n'a pas marché, n'est-ce pas?
Et quoi GroupBy
? Essayons ça:
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);
}
}
Production:
[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
Encore une fois: n'a pas fonctionné.
Si vous y réfléchissez, il serait logique Distinct
d'utiliser a HashSet<T>
(ou équivalent) en interne, et GroupBy
d'utiliser quelque chose comme un en Dictionary<TKey, List<T>>
interne. Cela pourrait-il expliquer pourquoi ces méthodes ne fonctionnent pas? Essayons ça:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Production:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Ouais ... commence à avoir un sens?
Espérons qu'à partir de ces exemples, il est clair pourquoi l'inclusion d'un approprié GetHashCode
dans toute IEqualityComparer<T>
implémentation est si importante.
Réponse originale
Élargissement de la réponse d'orip :
Il y a quelques améliorations qui peuvent être apportées ici.
- Tout d'abord, je prendrais un
Func<T, TKey>
au lieu de Func<T, object>
; cela empêchera l'encadrement des clés de type valeur dans le réel keyExtractor
lui-même.
- Deuxièmement, j'ajouterais en fait une
where TKey : IEquatable<TKey>
contrainte; cela évitera le boxing dans l' Equals
appel ( object.Equals
prend un object
paramètre; vous avez besoin d'une IEquatable<TKey>
implémentation pour prendre un TKey
paramètre sans le boxing). Clairement, cela peut poser une restriction trop sévère, vous pouvez donc créer une classe de base sans la contrainte et une classe dérivée avec elle.
Voici à quoi pourrait ressembler le code résultant:
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>
qui laisse deGetHashCode
côté est tout simplement cassé.