Sobre la importancia de GetHashCode
Otros ya han comentado el hecho de que cualquier IEqualityComparer<T>
implementación personalizada realmente debería incluir un GetHashCode
mé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 Equals
mé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 Distinct
uso de un algoritmo O (N 2 ) en el peor de los casos en lugar de uno O (N)!
Afortunadamente, este no es el caso. Distinct
no solo usa Equals
; Se usa GetHashCode
tambié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 Distinct
usar 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 Value
elementos con la misma Name
propiedad, 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 Distinct
usar un HashSet<T>
(o equivalente) internamente, y GroupBy
usar 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 GetHashCode
en 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 keyExtractor
propio.
- Segundo, en realidad agregaría una
where TKey : IEquatable<TKey>
restricción; esto evitará el encajonamiento en la Equals
llamada ( object.Equals
toma un object
parámetro; necesita una IEquatable<TKey>
implementación para tomar un TKey
pará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 quedaGetHashCode
fuera está simplemente roto.