Várias funções Linq.Enumerable recebem um IEqualityComparer<T>
. Existe uma classe de wrapper conveniente que adapte a delegate(T,T)=>bool
para implementar IEqualityComparer<T>
? É fácil escrever um (se você ignorar problemas ao definir um código hash correto), mas eu gostaria de saber se existe uma solução pronta para uso.
Especificamente, quero definir operações em Dictionary
s, usando apenas as Chaves para definir associação (mantendo os valores de acordo com regras diferentes).
IEqualityComparer<T>
que deixa deGetHashCode
fora é simplesmente quebrado.Sobre a importância de
GetHashCode
Outros já comentaram o fato de que qualquer
IEqualityComparer<T>
implementação personalizada deve realmente incluir umGetHashCode
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 umEquals
método. Como você determina se um item já foi digitalizado em uma sequência, se você apenas o possuiEquals
? Você enumera toda a coleção de valores que você já examinou e verifica a correspondência. Isso resultaria noDistinct
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 usaEquals
; ele usaGetHashCode
também. De fato, absolutamente não funciona corretamente sem umIEqualityComparer<T>
que forneça um adequadoGetHashCode
. Abaixo está um exemplo artificial que ilustra isso.Digamos que eu tenha o seguinte tipo:
Agora diga que tenho um
List<Value>
e quero encontrar todos os elementos com um nome distinto. Este é um caso de uso perfeito paraDistinct
usar um comparador de igualdade customizado. Então, vamos usar aComparer<T>
classe da resposta de Aku :Agora, se tivermos um monte de
Value
elementos com a mesmaName
propriedade, todos eles deverão ser recolhidos em um valor retornado porDistinct
, certo? Vamos ver...Resultado:
Hmm, isso não funcionou, não é?
Que tal
GroupBy
? Vamos tentar isso:Resultado:
Mais uma vez: não funcionou.
Se você pensar bem, faria sentido
Distinct
usar umHashSet<T>
(ou equivalente) internamente eGroupBy
usar algo como umDictionary<TKey, List<T>>
internamente. Isso poderia explicar por que esses métodos não funcionam? Vamos tentar isso:Resultado:
Sim ... começando a fazer sentido?
Felizmente, a partir desses exemplos, fica claro por que a inclusão de um apropriado
GetHashCode
em qualquerIEqualityComparer<T>
implementação é tão importante.Resposta original
Expandindo a resposta do orip :
Existem algumas melhorias que podem ser feitas aqui.
Func<T, TKey>
vez deFunc<T, object>
; isso impedirá o encaixe das chaves de tipo de valor nokeyExtractor
próprio real .where TKey : IEquatable<TKey>
restrição; isso impedirá o encaixe naEquals
chamada (object.Equals
aceita umobject
parâmetro; você precisa de umaIEquatable<TKey>
implementação para pegar umTKey
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:
fonte
StrictKeyEqualityComparer.Equals
método parece ser o mesmo queKeyEqualityComparer.Equals
. ATKey : IEquatable<TKey>
restrição faz oTKey.Equals
trabalho de maneira diferente?TKey
pode ser de qualquer tipo arbitrário, o compilador usará o método virtualObject.Equals
que exigirá o boxe de parâmetros de tipo de valor, por exemploint
,. No último caso, no entanto, comoTKey
é restrito à implementaçãoIEquatable<TKey>
,TKey.Equals
será utilizado o método que não exigirá nenhum boxe.StringKeyEqualityComparer<T, TKey>
também.Quando você deseja personalizar a verificação de igualdade, 99% do tempo está interessado em definir as chaves a serem comparadas, não a comparação em si.
Essa poderia ser uma solução elegante (conceito do método de classificação de lista do Python ).
Uso:
A
KeyEqualityComparer
classe:fonte
Func<T, int>
para fornecer o código de hash para umT
valor (como sugerido em, por exemplo, a resposta de Ruben ). Caso contrário, aIEqualityComparer<T>
implementação que você deixou é bastante interrompida, especialmente no que diz respeito à sua utilidade nos métodos de extensão LINQ. Veja minha resposta para uma discussão sobre o motivo disso.Receio que não exista esse invólucro pronto para uso. No entanto, não é difícil criar um:
fonte
private readonly Func<T, int> _hashCodeResolver
que também deve ser passado no construtor e ser usado noGetHashCode(...)
métodoobj.ToString().ToLower().GetHashCode()
vez deobj.GetHashCode()
?IEqualityComparer<T>
usam invariavelmente o hash nos bastidores (por exemplo, GroupBy, Distinct, Except, Join, etc) do LINQ e o contrato da MS em relação ao hash são quebrados nesta implementação. Aqui está o trecho da documentação da MS: "São necessárias implementações para garantir que, se o método Equals retornar true para dois objetos x e y, o valor retornado pelo método GetHashCode para x deve ser igual ao valor retornado para y". Veja: msdn.microsoft.com/en-us/library/ms132155O mesmo que a resposta de Dan Tao, mas com algumas melhorias:
Confia em
EqualityComparer<>.Default
fazer a comparação real para evitar boxe para os tipos de valorstruct
que foram implementadosIEquatable<>
.Desde que
EqualityComparer<>.Default
usado, ele não explodenull.Equals(something)
.O invólucro estático fornecido em torno do
IEqualityComparer<>
qual terá um método estático para criar a instância do comparador - facilita a chamada. Compararcom
Adicionada uma sobrecarga para especificar
IEqualityComparer<>
a chave.A classe:
você pode usá-lo assim:
Pessoa é uma classe simples:
fonte
Com extensões: -
fonte
A resposta do orip é ótima.
Aqui está um pequeno método de extensão para facilitar ainda mais:
fonte
Eu vou responder minha própria pergunta. Para tratar Dicionários como conjuntos, o método mais simples parece ser aplicar operações de conjunto a dict.Keys e depois converter novamente em Dicionários com Enumerable.ToDictionary (...).
fonte
A implementação em (texto em alemão) Implementing IEqualityCompare com expressão lambda se preocupa com valores nulos e usa métodos de extensão para gerar IEqualityComparer.
Para criar um IEqualityComparer em uma união Linq, basta escrever
O comparador:
Você também precisa adicionar um método de extensão para dar suporte à inferência de tipo
fonte
Apenas uma otimização: podemos usar o EqualityComparer pronto para uso para comparações de valor, em vez de delegá-lo.
Isso também tornaria a implementação mais limpa, já que a lógica de comparação real agora permanece em GetHashCode () e Equals (), que você já pode ter sobrecarregado.
Aqui está o código:
Não se esqueça de sobrecarregar os métodos GetHashCode () e Equals () no seu objeto.
Este post me ajudou: c # comparar dois valores genéricos
Sushil
fonte
A resposta do orip é ótima. Expandindo a resposta do orip:
Eu acho que a chave da solução é usar "Método de extensão" para transferir o "tipo anônimo".
Uso:
fonte
Isso torna possível selecionar uma propriedade com lambda assim:
.Select(y => y.Article).Distinct(x => x.ArticleID);
fonte
Não conheço uma classe existente, mas algo como:
Nota: Na verdade, eu ainda não compilei e executei isso; portanto, pode haver um erro de digitação ou outro bug.
fonte