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 GetHashCodemé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 Equalsmé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' Distinctutilisation d'un algorithme O (N 2 ) du pire des cas au lieu d'un algorithme O (N)!
Heureusement, ce n'est pas le cas. Distinctne pas simplement utiliser Equals; il utilise GetHashCodeaussi. 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 Distinctutiliser 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 Nameproprié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 Distinctd'utiliser a HashSet<T>(ou équivalent) en interne, et GroupByd'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é GetHashCodedans 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 keyExtractorlui-même.
- Deuxièmement, j'ajouterais en fait une
where TKey : IEquatable<TKey>contrainte; cela évitera le boxing dans l' Equalsappel ( object.Equalsprend un objectparamètre; vous avez besoin d'une IEquatable<TKey>implémentation pour prendre un TKeyparamè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 deGetHashCodecôté est tout simplement cassé.