Problema: Distinct() não sabe filtrar por campo
\n\nImagine: você tem uma lista de clientes e nela existem duplicatas por email. Você escreve:
\n\nvar uniqueClients = clients.Distinct().ToList();\n\nE descobre, surpreso, que as duplicatas não desapareceram. Por quê? Porque o Distinct() padrão compara objetos inteiros por referência (ou por Equals, se você o sobrescreveu). Ele não sabe que para você "unicidade" significa um Email único, e não uma instância única da classe.
Esta pergunta é uma das mais populares no Stack Overflow sobre LINQ (mais de 500 mil visualizações). E hoje vamos analisar 3 formas práticas de resolver este problema em C#. Vamos lá!
\n\nMétodo 1. GroupBy + First (clássico)
\n\nO método mais simples e compreensível, que não requer instalação de bibliotecas externas. A ideia: agrupar os objetos pela propriedade desejada e, em seguida, pegar o primeiro elemento de cada grupo.
\n\nvar uniqueByEmail = clients\n .GroupBy(c => c.Email)\n .Select(g => g.First())\n .ToList();\n\nComo funciona:
\n- \n
GroupBy(c => c.Email)— divide a coleção em grupos, onde a chave é o email. \n Select(g => g.First())— de cada grupo, pega o primeiro objeto encontrado. \n - As demais duplicatas são descartadas. \n
Vantagens:
\n- \n
- Funciona "de fábrica" (apenas
using System.Linq;). \n - Intuitivo até para iniciantes. \n
- Preserva a ordem original dos elementos (o primeiro objeto encontrado permanece). \n
Desvantagens:
\n- \n
- Um pouco mais lento em grandes volumes de dados (precisa criar grupos). \n
- Código um pouco mais verboso do que o desejado. \n
Método 2. MoreLinq e DistinctBy (elegância)
\n\nSe você não tem receio de bibliotecas externas, o MoreLinq oferece o método ideal DistinctBy. Ele faz exatamente o que você espera do Distinct — filtra por propriedade.
Instalação via NuGet:
\nInstall-Package MoreLinq\n\nUso:
\nusing MoreLinq;\n\nvar uniqueByEmail = clients.DistinctBy(c => c.Email).ToList();\n\nUma linha e está pronto. Internamente, o DistinctBy usa HashSet e opera em O(n).
Vantagens:
\n- \n
- Código extremamente conciso e legível. \n
- Alta performance. \n
- Biblioteca bem testada (mais de 10 milhões de downloads). \n
Desvantagens:
\n- \n
- É necessário adicionar uma dependência ao projeto. \n
- Nem todas as equipes permitem bibliotecas externas. \n
Método 3. IEqualityComparer personalizado (controle máximo)
\n\nEste método é adequado quando você não quer adicionar uma biblioteca, mas deseja reutilizar a lógica de comparação em vários lugares. Criamos nosso próprio comparador:
\n\npublic class ClientEmailComparer : IEqualityComparer<Client>\n{\n public bool Equals(Client x, Client y)\n {\n if (ReferenceEquals(x, y)) return true;\n if (x is null || y is null) return false;\n return x.Email == y.Email;\n }\n\n public int GetHashCode(Client obj)\n {\n return obj.Email?.GetHashCode() ?? 0;\n }\n}\n\nUso:
\nvar comparer = new ClientEmailComparer();\nvar uniqueByEmail = clients.Distinct(comparer).ToList();\n\nVantagens:
\n- \n
- Controle total sobre a lógica de comparação. \n
- O comparador pode ser reutilizado em
Contains,Except,Union, etc. \n - Não requer bibliotecas externas. \n
Desvantagens:
\n- \n
- Muito código boilerplate. \n