Problema: Distinct() no sabe filtrar por campo
\n\nImagine: tiene una lista de clientes y en ella se han colado duplicados por email. Usted escribe:
\n\nvar uniqueClients = clients.Distinct().ToList();\n\nY con sorpresa descubre que los duplicados no han desaparecido. ¿Por qué? Porque el Distinct() estándar compara objetos completos por referencia (o por Equals, si lo ha redefinido). No sabe que para usted la "unicidad" es un Email único, no una instancia única de la clase.
Esta pregunta es una de las más populares en Stack Overflow sobre LINQ (más de 500 mil visitas). Y hoy analizaremos 3 formas prácticas de resolver este problema en C#. ¡Allá vamos!
\n\nMétodo 1. GroupBy + First (el clásico)
\n\nEl método más simple y claro, que no requiere instalar bibliotecas externas. La idea: agrupar los objetos por la propiedad deseada y luego tomar el primer elemento de cada grupo.
\n\nvar uniqueByEmail = clients\n .GroupBy(c => c.Email)\n .Select(g => g.First())\n .ToList();\n\nCómo funciona:
\n- \n
GroupBy(c => c.Email)— divide la colección en grupos, donde la clave es el email. \n Select(g => g.First())— de cada grupo toma el primer objeto que encuentra. \n - Los demás duplicados se descartan. \n
Ventajas:
\n- \n
- Funciona "de serie" (solo
using System.Linq;). \n - Intuitivo incluso para principiantes. \n
- Conserva el orden original de los elementos (permanece el primer objeto encontrado). \n
Desventajas:
\n- \n
- Un poco más lento en grandes volúmenes de datos (necesita crear grupos). \n
- El código es un poco más verboso de lo deseado. \n
Método 2. MoreLinq y DistinctBy (elegancia)
\n\nSi no le teme a las bibliotecas externas, MoreLinq ofrece el método ideal DistinctBy. Hace exactamente lo que espera de Distinct: filtrar por propiedad.
Instalación mediante NuGet:
\nInstall-Package MoreLinq\n\nUso:
\nusing MoreLinq;\n\nvar uniqueByEmail = clients.DistinctBy(c => c.Email).ToList();\n\nUna sola línea y todo está listo. Internamente, DistinctBy usa HashSet y funciona en O(n).
Ventajas:
\n- \n
- Código extremadamente conciso y legible. \n
- Alto rendimiento. \n
- Biblioteca bien probada (más de 10 millones de descargas). \n
Desventajas:
\n- \n
- Requiere agregar una dependencia al proyecto. \n
- No todos los equipos permiten bibliotecas externas. \n
Método 3. IEqualityComparer propio (máximo control)
\n\nEste método es adecuado cuando no desea agregar una biblioteca, pero quiere reutilizar la lógica de comparación en varios lugares. Creamos nuestro propio 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\nVentajas:
\n- \n
- Control total sobre la lógica de comparación. \n
- El comparador se puede reutilizar en
Contains,Except,Union, etc. \n - No requiere bibliotecas externas. \n
Desventajas:
\n- \n
- Mucho código repetitivo. \n