Sobre la importancia de GetHashCode
Otros ya han comentado el hecho de que cualquier IEqualityComparer<T>implementación personalizada realmente debería incluir un GetHashCodemétodo ; pero nadie se molestó en explicar por qué con ningún detalle.
Este es el por qué. Su pregunta menciona específicamente los métodos de extensión LINQ; Casi todos estos dependen de códigos hash para funcionar correctamente, ya que utilizan tablas hash internamente para mayor eficiencia.
Toma Distinct, por ejemplo. Considere las implicaciones de este método de extensión si todo lo que utilizara fuera un Equalsmétodo. ¿Cómo determina si un elemento ya ha sido escaneado en una secuencia si solo lo ha hecho Equals? Enumera toda la colección de valores que ya ha visto y busca una coincidencia. ¡Esto daría como resultado el Distinctuso de un algoritmo O (N 2 ) en el peor de los casos en lugar de uno O (N)!
Afortunadamente, este no es el caso. Distinctno solo usa Equals; Se usa GetHashCodetambién. De hecho, absolutamente no funciona correctamente sin un IEqualityComparer<T>que proporcione un adecuadoGetHashCode . A continuación se muestra un ejemplo artificial que ilustra esto.
Digamos que tengo el siguiente 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);
}
}
Ahora di que tengo un List<Value>y quiero encontrar todos los elementos con un nombre distinto. Este es un caso de uso perfecto para Distinctusar un comparador de igualdad personalizado. Entonces usemos la Comparer<T>clase de la respuesta de Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Ahora, si tenemos un montón de Valueelementos con la misma Namepropiedad, todos deberían colapsar en un valor devuelto por Distinct, ¿verdad? Veamos...
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);
}
Salida:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, eso no funcionó, ¿verdad?
¿Qué hay de GroupBy? Probemos eso:
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);
}
}
Salida:
[CLAVE = 'x: 1346013431']
x: 1346013431
[CLAVE = 'x: 1388845717']
x: 1388845717
[CLAVE = 'x: 1576754134']
x: 1576754134
[CLAVE = 'x: 1104067189']
x: 1104067189
[CLAVE = 'x: 1144789201']
x: 1144789201
[CLAVE = 'x: 1862076501']
x: 1862076501
[CLAVE = 'x: 1573781440']
x: 1573781440
[CLAVE = 'x: 646797592']
x: 646797592
[CLAVE = 'x: 655632802']
x: 655632802
[CLAVE = 'x: 1206819377']
x: 1206819377
De nuevo: no funcionó.
Si lo piensa, tendría sentido Distinctusar un HashSet<T>(o equivalente) internamente, y GroupByusar algo como un Dictionary<TKey, List<T>>interno. ¿Podría esto explicar por qué estos métodos no funcionan? Intentemos esto:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Salida:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Sí ... ¿empieza a tener sentido?
Esperemos que de estos ejemplos quede claro por qué es tan importante incluir un apropiado GetHashCodeen cualquier IEqualityComparer<T>implementación.
Respuesta original
Ampliando la respuesta de orip :
Hay un par de mejoras que se pueden hacer aquí.
- Primero, tomaría un en
Func<T, TKey>lugar de Func<T, object>; Esto evitará el encajonamiento de las claves de tipo de valor en el keyExtractorpropio.
- Segundo, en realidad agregaría una
where TKey : IEquatable<TKey>restricción; esto evitará el encajonamiento en la Equalsllamada ( object.Equalstoma un objectparámetro; necesita una IEquatable<TKey>implementación para tomar un TKeyparámetro sin encajonarlo). Claramente, esto puede suponer una restricción demasiado severa, por lo que podría hacer una clase base sin la restricción y una clase derivada con ella.
Así es como se vería el código resultante:
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>que quedaGetHashCodefuera está simplemente roto.