Tips & Tricks | Combinar dos listas sin duplicados con Linq

En muchas ocasiones es necesario combinar dos listas de elementos del mismo tipo de objeto sin tener la certeza de que puedan o no existir duplicados. Realizar esta combinación es, a priori, una tarea sencilla puesto que podemos resolverla usando un bucle y realizando todas las comprobaciones pertinentes pero, ¿es la forma más elegante y legible? Gracias a Linq podemos resolver esta tarea, y otras muchas, sin necesidad de escribir grandes cantidades de código y haciéndolo mucho más legible y mantenible.

En este artículo quiero plasmar algunas de las opciones (hay más incluso) de las que disponemos para combinar listas y poder controlar (o no) si queremos mantener los duplicados o eliminarlos. Para ello, voy a empezar desde lo más básico, que es combinar las listas, hasta la solución «adecuada» (al final del artículo).

Si lo prefieres, puedes ver un vídeo en mi canal en el que explico cada uno de los siguientes puntos.

Contexto de los ejemplos

Para poner en contexto los ejemplos que voy a mostrar, hay que empezar por el modelo que voy a usar y las listas que voy a combinar,

Modelo

public class Saint
{
public int Id { get; set; }
public string Name { get; set; }
public string Constellation { get; set; }
public string Description { get; set; }
}

Listas

static List<Saint> saintsList1 = new List<Saint>
{
new Saint { Id=1, Name="Seiya", Constellation="Pegasus", Description="Bronze Saint" },
new Saint { Id=2, Name="Siryu", Constellation="Dragon", Description="Bronze Saint" },
};
static List<Saint> saintsList2 = new List<Saint>
{
new Saint { Id=3, Name="Hyoga", Constellation="Swan", Description="Bronze Saint" },
new Saint { Id=4, Name="Shun", Constellation="Andromeda", Description="Bronze Saint" },
new Saint { Id=5, Name="Ikki", Constellation="Phoenix", Description="Bronze Saint" }
};
static List<Saint> saintsList3 = new List<Saint>
{
new Saint { Id=1, Name="Seiya", Constellation="Pegasus", Description="Bronze Saint" },
new Saint { Id=3, Name="Hyoga", Constellation="Swan", Description="Bronze Saint" },
new Saint { Id=4, Name="Shun", Constellation="Andromeda", Description="Bronze Saint" },
};
static List<Saint> saintsList4 = new List<Saint>
{
new Saint { Id=1, Name="Seiya", Constellation="Pegasus", Description="DUPLICATED SAINT" },
new Saint { Id=6, Name="Dohko", Constellation="Libra", Description="Gold Saint" },
new Saint { Id=7, Name="Shaka", Constellation="Virgo", Description="Gold Saint" }
};

1. Combinación simple de listas

Para poder combinar dos listas, Linq nos provee del método «Union« que nos permite, a través de una instancia de una colección, añadirle los elementos de otra colección del mismo tipo. En este caso, estoy usando List<Saint> para combinar las listas saintList1 y saintList2, definidas anteriormente.

var unionList = saintList1.Union(saintList2);

Como podemos ver, el resultado es la adición de los elementos de la lista saintList2 a la lista saintList1, pero en este caso no estamos tomando en cuenta que puedan existir duplicados con lo que, en caso de haberlos, aparecerán en el resultado

En el caso de que dos elementos duplicados tengan la misma referencia, es decir que sea exactamente la misma instancia del modelo la que se encuentre en ambas lista, no aparecerá en el resultado final ya que Linq por defecto será capaz de identificarla y omitirla.

2. Combinación simple con duplicados

Como ya indiqué en el caso anterior, si hacemos la unión de dos listas en las que tenemos un elemento duplicado, que no sea la misma instancia, en el resultado aparecerán ambos elementos. Esto ocurrirá si combinamos las listas saintList1 y saintList3

var unionList = saintList1.Union(saintList3);

3. Combinación simple SIN duplicados

Tal y como indiqué, si el elemento duplicado en las dos listas es exactamente la misma instancia, será Linq quien la omita del resultado puesto que es capaz identificar que ese elemento es el mismo. Veamos qué ocurre si combinamos la lista saintList1 con ella misma.

var unionList = saintList1.Union(saintList1);

Como se puede apreciar, la lista resultante contiene los elementos una única vez ya que, al combinar la lista con ella misma, Linq ha detectado que la instancia de cada uno de los elementos es la misma.

4. Combinación simple usando Concat + Distinct

Una alternativa que podemos usar, algo más engorrosa, es combinar «Concat» con «Distinct». Pero como podemos comprobar, vamos a obtener el mismo resultado que con el «Union» ya que el «Distinct» por defecto no sólo va a tener en cuenta los valores de las propiedades de los elementos, sino la instancia en sí, con lo que, en caso de que tengamos elementos iguales pero de diferente instancia, aparecerán en el resultado. Veamos qué ocurre si realizamos la misma combinación que en el punto 2, combinando las listas saintList1 con saintList3.

var unionList = saintList1.Concat(saintList3).Distinct().ToList();

5. Combinación simple usando Concat + Distinct sin duplicados

En este caso, vamos a ver que el principio de funcionamiento de «Concat» + «Distinct» es el mismo que el del «Union» al unir la lista saintList1 con ella misma, es decir, que si los elementos duplicados se corresponden con la misma instancia, Linq los detectará y al hacer el «Distinct» los eliminará del resultado.

var unionList = saintList1.Concat(saintList1).Distinct().ToList();

Combinación sin duplicados (Uso de comparer)

En los ejemplos anteriores hemos visto que es posible combinar dos listas y obtener un resultado sin duplicados, aunque sólo en el caso de que dichos elementos duplicados sean la misma instancia. ¿Pero, qué ocurre si no podemos trabajar con listas con la misma instancia? De hecho, por lo general, esta será la situación con lo que las soluciones anteriormente propuestas no nos servirían de nada.

En este caso, tendremos que hacer uso de un nuevo elemento, la interfaz IEqualityComparer<T> que nos permitirá establecer las reglas de comparación para determinar si dos elementos son iguales o no, sin tener en cuenta la instancia en sí.

public class SaintComparer : IEqualityComparer<Saint>
{
public bool Equals(Saint item1, Saint item2)
{
return item1.Name == item2.Name;
}

public int GetHashCode(Saint item)
{
int hCode = item.Name.Length;
return hCode.GetHashCode();
}
}

Como se puede ver, he definido la clase SaintComparer que implementa la interfaz IEqualityComparer<Saint> para realizar esta tarea. Esta clase tendrá que definir los métodos Equals y GetHashCode con las reglas que nosotros queramos que determinen si dos elementos son iguales o no. En este caso, por tratarse de un ejemplo muy simple, he definido que dos elementos son iguales sin tienen el mismo valor en la propiedad «Name» y el HashCode lo he reducido a devolver el tamaño de esta misma propiedad.

De esta forma, cuando combinemos dos listas con elementos cuya propiedad «Name» coincida, obtendremos como resultado una lista sin esos duplicados basándonos en las reglas definidas anteriormente. Veamos el ejemplo.

var unionList = list1.Union(list2, new SaintComparer());

Como se puede observar, a la hora de llamar la método «Union» le hemos añadido una instancia de la clase «SaintComparer» para que actúe como mediador en la comparación y sea quien determine los elementos coincidentes. De esta forma, ha identificado como duplicados a los dos elementos con la propiedad «Name» con valor «Seiya» aunque tienen diferente descripción y se ha quedado sólo con uno de ellos.

Resumen

Como hemos podido comprobar, con muy pocas líneas de código podemos escribir un código fácilmente legible para combinar dos listas sin duplicados, evitando así tener que escribir toda la lógica de dicha combinación a mano.

Espero que os sea de utilidad.

Enjoy coding!