Disclaimer: esse artigo é sobre um detalhe tão específico do Framework, que vale mais pelas fotos do que pelo conteúdo. Mas se você for uma das 19 pessoas no mundo que querem usar Distinct() em Linq para evitar instâncias repetidas no seu resultado, além das belas paisagens o código pode te ajudar.

Estou eu encalhado aqui no Galeão, voltando de uma semana nas Európias – eu e Minha Dona (letra maiúscula que não sou doido) fomos conhecer Paris e Londres. Nosso vôo pra Brasília só sai daqui a 6 horas; eu não tô fazendo nada mesmo, então pensei em escrever um pouco sobre Linq, Distinct(), IEqualityComparer e GetHashCode().
Uma aplicação nossa lê de um arquivo um monte de registros de produtos pra dentro de uma
List<Produtos>. Aí eu fui jogar estes produtos na tabela de Produtos no BD, mas como havia registros repetidos no arquivo, apareceram vários registros para o mesmo produto no banco.
“Bem, é só fazer o DISTINCT na lista”. Ok. Fui olhar o método
Distinct() do Linq e achei duas assinaturas: uma recebe uma expressão lambda cujo resultado, ao ser avaliado para cada registro da lista, define os registros distintos; e outra recebe uma instância de
IEqualityComparer, a qual nem dei uma segunda olhada.
Pra gerar minha lista de produtos distintos, escrevi:
// 1ª tentativa: Distinct(p => p) pra retornar uma lista de produtos sem repetidos
List<Produto> listaSemRepetidos = listaProdutos.Distinct(p => p);

Estranhamente, isto fez com que *todos* os objetos em
listaProdutos aparececem em
listaSemRepetidos. Pensando melhor, faz sentido.
Distinct() tem que ver se os objetos são iguais para decidir se eles são repetidos, então deve chamar
Equals() – qualquer classe tem
Equals(), que é herdado de
System.Object - e na implementação default de
Equals() dois objetos são iguais se apontam para a mesma instância, e não se seus campos contem o mesmo valor.
Antes de ir escovar bit e partir pra uma implementação customizada de
Equals() na minha classe
Produtos, resolvi dar uma olhada com mais calma na segunda assinatura do
Distinct(), aquela que recebe uma instância de
IEqualityComparer. Esta interface contem dois métodos:
bool Equals(<T>, <T>) e
int GetHashCode(). “Morreu”, pensei eu. “É só implementar a interface e na implementação do
Equals(<T>,<T>), comparar os campos das instâncias recebidas”. Minha implementação ficou assim:
public class ComparadorProdutos : IEqualityComparer<AcessoDados.Entidades.Produto>
{
public bool Equals(AcessoDados.Entidades.Produto p1, AcessoDados.Entidades.Produto p2)
{
// Trata os casos nos quais há objetos nulos
if(p1 == null && p2 == null) return true;
if(p1 == null && p2 != null) return false;
if(p1 != null && p2 == null) return false;
// Se nenhum dos dois é nulo, retorna true se os campos “importantes” contém o mesmo valor
return
(
p1.CodigoProduto == p2.CodigoProduto &&
p1.DataInicial == p2.DataInicial &&
p1.DataFinal == p2.DataFinal &&
p1.CodigoNCM == p2.CodigoNCM &&
p1.Descricao == p2.Descricao &&
p1.UnidadeMedida == p2.UnidadeMedida &&
p1.AliquotaICMS == p2.AliquotaICMS &&
p1.AliquotaIPI == p2.AliquotaIPI &&
p1.ReducaoBaseCalculoICMS == p2.ReducaoBaseCalculoICMS &&
p1.BaseCalculoICMSSubstTrib == p2.BaseCalculoICMSSubstTrib &&
p1.IdCliente == p2.IdCliente
);
}
// Retorna qualquer coisa em GetHashCode() pq não vou usar esse trem mesmo
public int GetHashCode(AcessoDados.Entidades.Produto p)
{
if(p == null) return -1;
else return p.GetHashCode();
}
}
Na implementação do
GetHashCode() coloquei qualquer leseira, porque o que o
Distinct() ia usar mesmo era o
Equals(). Testei com um programinha console, tudo ok, agora é só chamar o
Distinct() passando uma instância de
ComparadorProdutos.
// 2ª tentativa: Passando uma implementação de IEqualityComparer para Distinct()
List<Produto> listaSemRepetidos = listaProdutos.Distinct(new ComparadorProdutos());

E de novo voltaram *todos* os registros. Caraca. Testei de um lado, do outro, volta pra aplicaçãozinha console pra ver se o
Equals() não tem alguma falha, e tudo ok. Só não funciona. Finalmente entreguei os pontos e fui consultar
A Fonte De Toda A Sabedoria. E realmente encontrei um artigo “LINQ: Distinct() does not work as expected”, de outro sofredor que passou pela mesma novela. Mas antes de recorrer ao Google, ele colocou um breakpoint em cada um dos métodos da sua implementação de
IEqualityComparer e viu que, em vez de chamar
Equals(), o método
Distinct() chama
GetHashCode() para verificar se os objetos recebidos são iguais!!! Se os objetos tem o mesmo valor para
GetHashCode() então eles são considerados iguais, e é assim que o
Distinct() separa objetos "repetidos" e "únicos". Ele até cita exemplos de boas implementações para o
GetHashCode(), mas a melhor dica quanto a isto é que o próprio .NET Framework já fornece uma boa implementação: a usada para tipos anônimos. Então na classe ComparadorProdutos mudei a minha implementação do
GetHashCode() para retornar o
hash code de um tipo anônimo com os campos da classe
Produto que eu queria comparar:
public int GetHashCode(AcessoDados.Entidades.Produto p)
{
if(p == null)
return 0;
else
return new
{
p.CodigoProduto,
p.DataInicial,
p.DataFinal,
p.CodigoNCM,
p.Descricao,
p.UnidadeMedida,
p.AliquotaICMS,
p.AliquotaIPI,
p.ReducaoBaseCalculoICMS,
p.BaseCalculoICMSSubstTrib,
p.IdCliente
}.GetHashCode();
}
Se duas instâncias da classe
Produto tem os mesmos valores nestes campos, seus
hash codes serão iguais. E aí meu
Distinct(new ComparadorProdutos) finalmente funcionou.

Cris me ajudando na redação do post no Galeão
É, nada como 6 horas de aeroporto pra gastar…
[]s,
GB