Sobre a importância de GetHashCode
Outros já comentaram o fato de que qualquer IEqualityComparer<T>implementação personalizada deve realmente incluir um GetHashCodemé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 Equalsmé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 Distinctuso de um algoritmo O (N 2 ) do pior caso, em vez de um algoritmo O (N)!
Felizmente, este não é o caso. Distinctnão apenas usa Equals; ele usa GetHashCodetambé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 Distinctusar 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 Valueelementos com a mesma Namepropriedade, 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 Distinctusar um HashSet<T>(ou equivalente) internamente e GroupByusar 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 GetHashCodeem 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 keyExtractorpróprio real .
- Segundo, eu adicionaria uma
where TKey : IEquatable<TKey>restrição; isso impedirá o encaixe na Equalschamada ( object.Equalsaceita um objectparâmetro; você precisa de uma IEquatable<TKey>implementação para pegar um TKeyparâ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 deGetHashCodefora é simplesmente quebrado.