CodeGym /Cursos /C# SELF /Operaciones Union, ...

Operaciones Union, Intersect, Except en LINQ

C# SELF
Nivel 33 , Lección 3
Disponible

1. Introducción

Vamos con un tema muy interesante y súper usado en la práctica: operaciones de conjuntos en LINQ — Union, Intersect, Except. Te dejan trabajar con colecciones como si estuvieras haciendo álgebra de conjuntos (o contando pegatinas en dos paquetes en el escritorio de un programador — que, la verdad, es casi lo mismo). Si nunca has oído hablar de álgebra de conjuntos (o ya lo olvidaste), puede sonar un poco intimidante. Pero en realidad es muy sencillo: imagina dos bolsas con frutas. El álgebra de conjuntos es una forma de ver qué frutas hay en ambas bolsas, cuáles solo en una, y cuáles hay si las juntas. Es como un juego con pegatinas: puedes juntar todo, buscar las que coinciden o quitar unas de otras. ¡Eso es todo!

Práctica: Estos métodos son súper útiles cuando necesitas unir resultados de dos consultas diferentes, encontrar elementos comunes o ver la diferencia entre colecciones. Por ejemplo:

  • Hacer una lista general de todos los productos que aparecen ya sea en la lista de ventas o en la de compras.
  • Encontrar productos que están en ambas listas — los que coinciden.
  • O ver qué productos hay en el almacén pero no se han vendido — ¡alguien se está quedando viejo ahí!

Tareas reales: El álgebra de conjuntos está en todas partes: filtrar usuarios por suscripciones, encontrar quién estuvo en diferentes grupos, buscar pedidos únicos o que se cruzan, comparar resultados de dos consultas distintas a la base de datos y mucho, mucho más.

2. Operación Union — unión de colecciones

Es fácil, así que vamos directo a la práctica. Supón que tenemos dos listas de productos:

List<string> warehouseProducts = new List<string> { "Leche", "Pan", "Queso", "Huevos" };
List<string> recentlySold = new List<string> { "Pan", "Queso", "Salami", "Té" };

¿Qué hace Union?

Union devuelve los elementos únicos que están al menos en una de las colecciones.
En resumen, es una unión.

var allProducts = warehouseProducts.Union(recentlySold);

foreach (var product in allProducts)
{
    Console.WriteLine(product);
}
// Mostrará: Leche, Pan, Queso, Huevos, Salami, Té

Así se ve visualmente:

Almacén Vendido Union (Unión)
Leche Pan Leche
Pan Queso Pan
Queso Salami Queso
Huevos Huevos
Salami

En términos de álgebra de conjuntos, Union es la operación "o": dame todo lo que esté en cualquier lado. Imagina dos cajas con diferentes tipos de té — si decides probar todos los sabores, no importa si algún té se repite — solo lo pruebas una vez.

Union elimina automáticamente los duplicados (basándose en la implementación de Equals y GetHashCode para el tipo de elemento). Si haces la unión para tus propias clases (por ejemplo, Product), asegúrate de que estos métodos estén bien implementados, si no Union puede comportarse raro: ¡los mismos elementos pueden considerarse diferentes!

No lo olvides: el orden de los elementos en la colección resultante mantiene el orden de la primera colección, y los nuevos se añaden al final (en el orden en que aparecen por primera vez en la segunda colección).

Ejemplo con objetos

Seguimos con nuestra app "Tienda". Tenemos dos listas de Product:

public class Product
{
    public string Name { get; set; }
    public string Category { get; set; }

    // ¡Para que Union/Intersect/Except funcione bien, hay que tener bien Equals y GetHashCode!
    public override bool Equals(object? obj) =>
        obj is Product other && Name == other.Name && Category == other.Category;

    public override int GetHashCode() => HashCode.Combine(Name, Category);
}

List<Product> stock = new()
{
    new Product { Name = "Leche", Category = "Lácteos" },
    new Product { Name = "Pan", Category = "Panadería" },
};

List<Product> sold = new()
{
    new Product { Name = "Pan", Category = "Panadería" },
    new Product { Name = "Salami", Category = "Embutidos" },
};

var all = stock.Union(sold);

// Ojo: los elementos no se duplican,
// aunque "parezcan" iguales, pero deben ser iguales según la lógica.

foreach (var p in all)
    Console.WriteLine($"{p.Name} ({p.Category})");

Error típico: Si no sobreescribes Equals/GetHashCode, Union va a considerar diferentes instancias con los mismos valores como elementos distintos.

3. Operación Intersect — intersección de colecciones

Intersect devuelve los elementos que están en todas las colecciones originales. Es como un "y" en álgebra de conjuntos.

Ejemplo

Recordemos las listas del ejemplo anterior:

var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
    Console.WriteLine(product);
}
// Mostrará: Pan, Queso

Visualización:

Almacén Vendido Intersect (Común)
Pan Pan Pan
Queso Queso Queso

¿Dónde sirve?

  • Quieres saber qué productos de tu almacén se vendieron hoy.
  • En entrevistas suelen preguntar: "¿Cómo encuentras la intersección de dos listas?" — ¡LINQ lo hace en una línea!
  • En apps de negocio, la intersección se usa mucho para filtrar por varios criterios a la vez.

Detalles y errores típicos

Si un elemento está varias veces en la colección, en el resultado solo habrá una vez ese elemento.
Para objetos complejos (como la clase Product) otra vez importa tener bien Equals/GetHashCode.

Ejemplo con objetos

var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Mostrará solo Pan (Panadería)

4. Operación Except — diferencia de colecciones

Except devuelve los elementos que están solo en la primera colección, pero no en la segunda.

Ejemplo

var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
    Console.WriteLine(product);
}
// Mostrará: Leche, Huevos

O sea, son los productos que están en el almacén, pero no se han vendido.

Visualización:

Almacén Vendido Except (solo almacén)
Leche Leche
Huevos Huevos
Pan Pan (omitido)
Queso Queso (omitido)

Analogía

Es como si quitaras de tu pila todos los documentos que ya enviaste — solo queda lo que aún tienes tú y nadie más.

Ejemplo con objetos

var unsold = stock.Except(sold);
foreach (var p in unsold)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Mostrará: Leche (Lácteos)

! Cosas no tan obvias

El método Except es sensible al orden: A.Except(B) — no es lo mismo que B.Except(A)! La primera colección es "de dónde restamos", la segunda es "qué quitamos".

5. Unión y composición de varias operaciones con LINQ

A veces una sola operación no basta. Por ejemplo, quieres mostrar productos que están solo en el almacén o solo en los vendidos — pero no en ambas colecciones a la vez (lo que se llama "diferencia simétrica").

Diferencia simétrica ("XOR" para conjuntos):

var onlyInOne = warehouseProducts.Except(recentlySold)
                   .Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
    Console.WriteLine(product);
}
// Mostrará: Leche, Huevos, Salami, Té

Para lógicas más complejas es cómodo combinar métodos LINQ:

// Encontrar productos que no están ni en el almacén ni entre los vendidos, solo en la lista "esperando llegada"
List<string> expected = new() { "Café", "Té", "Leche" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
    Console.WriteLine(product);
// Mostrará: Café

6. Trabajar con tipos personalizados y IEqualityComparer

A veces no quieres comparar objetos por todos los campos: por ejemplo, solo te importa el nombre del producto, la categoría ya no tanto. Para eso los métodos LINQ aceptan un parámetro extra — IEqualityComparer<T>, que define cómo comparar los elementos.

Ejemplo de comparador propio:

class ProductNameComparer : IEqualityComparer<Product>
{
    public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
    public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}

var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
    Console.WriteLine(p.Name); // Leche, Pan, Salami

Esto es muy útil si no quieres cambiar Equals/GetHashCode en todo el modelo, sino solo comparar objetos por una regla específica en un caso concreto.

7. Esquemas visuales y tablas

Vamos a resumir los usos (para listas A y B):

Operación Resultado
A.Union(B)
Todo lo que hay en A o B (elementos únicos).
A.Intersect(B)
Todo lo que hay en ambos (A y B).
A.Except(B)
Todo lo que hay solo en A, pero no en B.

Esquema (Venn Diagram, si pudiéramos dibujar):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: todo lo que está dentro de ambos círculos.
  • Intersect: solo la intersección (el centro).
  • Except: solo lo que está en el círculo A, sin tocar la intersección con B.

8. Errores típicos al usar Union, Intersect y Except

Error nº1: usar con objetos sin Equals y GetHashCode.
Si tu clase no tiene estos métodos sobreescritos, los métodos Union, Intersect y Except no van a funcionar bien: objetos con el mismo contenido se considerarán diferentes. Al final tendrás un resultado inesperado (y muchas veces inútil).

Error nº2: intentar comparar objetos por solo algunos campos sin IEqualityComparer.
Por ejemplo, si quieres comparar productos solo por el nombre y no por toda la estructura, así tal cual Intersect no lo va a entender. Sin un IEqualityComparer explícito, el resultado no será el esperado.

Error nº3: esperar un orden concreto de los elementos.
Muchos piensan que la colección final mantiene el orden de la unión o la intersección. Pero depende del método: Union mantiene el orden de la primera colección, pero Intersect y Except pueden devolver los elementos en un orden impredecible. Mejor no confíes en el orden.

Error nº4: ignorar el rendimiento con colecciones grandes.
Si tienes muchos datos, estos métodos pueden ir lentos. Piensa en agregar, filtrar o usar estructuras hash (HashSet, por ejemplo) para acelerar las operaciones.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION