Sobre a importância de GetHashCode
Outros já comentaram o fato de que qualquer IEqualityComparer<T>
implementação personalizada deve realmente incluir um GetHashCode
método ; mas ninguém se preocupou em explicar o porquê em detalhes.
Aqui está o porquê. Sua pergunta menciona especificamente os métodos de extensão LINQ; quase todos eles contam com códigos de hash para funcionar corretamente, porque utilizam tabelas de hash internamente para obter eficiência.
Tome Distinct
, por exemplo. Considere as implicações desse método de extensão se tudo o que ele utilizou fosse um Equals
método. Como você determina se um item já foi digitalizado em uma sequência, se você apenas o possui Equals
? Você enumera toda a coleção de valores que você já examinou e verifica a correspondência. Isso resultaria no Distinct
uso de um algoritmo O (N 2 ) do pior caso, em vez de um algoritmo O (N)!
Felizmente, este não é o caso. Distinct
não apenas usa Equals
; ele usa GetHashCode
também. De fato, absolutamente não funciona corretamente sem um IEqualityComparer<T>
que forneça um adequadoGetHashCode
. Abaixo está um exemplo artificial que ilustra isso.
Digamos que eu tenha o seguinte 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);
}
}
Agora diga que tenho um List<Value>
e quero encontrar todos os elementos com um nome distinto. Este é um caso de uso perfeito para Distinct
usar um comparador de igualdade customizado. Então, vamos usar a Comparer<T>
classe da resposta de Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Agora, se tivermos um monte de Value
elementos com a mesma Name
propriedade, todos eles deverão ser recolhidos em um valor retornado por Distinct
, certo? Vamos ver...
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);
}
Resultado:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, isso não funcionou, não é?
Que tal GroupBy
? Vamos tentar isso:
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);
}
}
Resultado:
[CHAVE = 'x: 1346013431']
x: 1346013431
[CHAVE = 'x: 1388845717']
x: 1388845717
[CHAVE = 'x: 1576754134']
x: 1576754134
[CHAVE = 'x: 1104067189']
x: 1104067189
[CHAVE = 'x: 1144789201']
x: 1144789201
[CHAVE = 'x: 1862076501']
x: 1862076501
[CHAVE = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377
Mais uma vez: não funcionou.
Se você pensar bem, faria sentido Distinct
usar um HashSet<T>
(ou equivalente) internamente e GroupBy
usar algo como um Dictionary<TKey, List<T>>
internamente. Isso poderia explicar por que esses métodos não funcionam? Vamos tentar isso:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Resultado:
x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Sim ... começando a fazer sentido?
Felizmente, a partir desses exemplos, fica claro por que a inclusão de um apropriado GetHashCode
em qualquer IEqualityComparer<T>
implementação é tão importante.
Resposta original
Expandindo a resposta do orip :
Existem algumas melhorias que podem ser feitas aqui.
- Primeiro, eu pegaria um em
Func<T, TKey>
vez de Func<T, object>
; isso impedirá o encaixe das chaves de tipo de valor no keyExtractor
próprio real .
- Segundo, eu adicionaria uma
where TKey : IEquatable<TKey>
restrição; isso impedirá o encaixe na Equals
chamada ( object.Equals
aceita um object
parâmetro; você precisa de uma IEquatable<TKey>
implementação para pegar um TKey
parâmetro sem encaixotá-lo). Claramente, isso pode representar uma restrição muito severa, para que você possa criar uma classe base sem a restrição e uma classe derivada com ela.
Aqui está a aparência do 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 deixa deGetHashCode
fora é simplesmente quebrado.