CodeGym /Cursos /C# SELF /Unir varias fuentes: Selec...

Unir varias fuentes: SelectMany en LINQ

C# SELF
Nivel 33 , Lección 2
Disponible

1. Introducción

Vamos a recordar cómo funciona el Select normal:

var names = users.Select(user => user.Name);

A cada usuario le corresponde su nombre — obtenemos una lista de nombres. ¡Fácil! ¿Pero qué pasa si queremos una lista de todos los pedidos de todos los usuarios?

var ordersList = users.Select(user => user.Orders);

¿Qué pasa aquí? Para cada usuario obtenemos una lista de pedidos. O sea, el resultado es una colección de colecciones (IEnumerable<List<Order>>). Es como una estantería con cajas: para llegar a cada pedido, tienes que abrir cada caja (la lista de pedidos del usuario) una por una.

Lo que queremos es "desempaquetar" todo el array para trabajar con él como una lista normal de pedidos. Aquí es donde SelectMany nos salva la vida.

Analogía: desempaquetar cajas

Imagina que todas tus cosas no están mezcladas, sino guardadas en cajas. Cada caja es una colección, por ejemplo, las compras de un usuario. Si quieres ver TODAS las cosas a la vez (por ejemplo, para hacer un inventario global), no vas a ir caja por caja y luego cosa por cosa, y así todo el rato. Es mucho más cómodo volcar el contenido de todas las cajas en un solo montón — y revisarlo ahí. Así funciona SelectMany.

2. ¿Qué es SelectMany?

SelectMany es un método de LINQ que coge una colección de colecciones y la convierte en una sola colección "plana". Por "plana" aquí queremos decir que la secuencia resultante ya no tiene listas internas — todos los elementos de las colecciones anidadas quedan al mismo nivel, como si los hubieras "sacado" fuera. Nada de muñecas rusas, solo una cadena larga de valores.

La signatura del método es así:


IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TResult>> selector
)

Funciona fácil: tienes una colección (por ejemplo, una lista de usuarios), y cada elemento tiene dentro otra colección (por ejemplo, la lista de pedidos de ese usuario). SelectMany coge cada usuario, saca sus pedidos y los junta en un solo flujo de pedidos — sin "envoltorio", sin anidamiento. Justo lo que necesitas cuando quieres pasar de una "lista de listas" a una lista normal.

3. Ejemplos sencillos

Ejemplo 1: Lista de usuarios y sus pedidos

Empezamos con nuestro modelo de aplicación, que vamos mejorando poco a poco:

// Supón que estas clases ya existen de lecciones anteriores
class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

class Order
{
    public int Id { get; set; }
    public string ProductName { get; set; }
}

Supón que tienes una lista de usuarios:

var users = new List<User>
{
    new User
    {
        Name = "Iván",
        Orders = new List<Order>
        {
            new Order { Id = 1, ProductName = "Libro" },
            new Order { Id = 2, ProductName = "Bolígrafo" }
        }
    },
    new User
    {
        Name = "María",
        Orders = new List<Order>
        {
            new Order { Id = 3, ProductName = "Portátil" }
        }
    }
};

Objetivo: obtener una lista de todos los productos que alguna vez han comprado todos los usuarios.

Select normal:

var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>

Aquí el resultado son muchas "cajas", en cada una — la lista de pedidos de un usuario.

SelectMany:

var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>

¡Ahora tenemos una sola colección "larga" de todos los pedidos!

Visualización

Usuario Pedidos
Iván Libro, Bolígrafo
María Portátil
  • Después de Select: [[Libro, Bolígrafo], [Portátil]] — lista de listas
  • Después de SelectMany: [Libro, Bolígrafo, Portátil] — lista única

Ejemplo 2: Trabajando con varios niveles de anidamiento

Si, por ejemplo, un pedido tiene una lista de productos (carrito), entonces puedes usar SelectMany varias veces — como una muñeca rusa.

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

class Order
{
    public int Id { get; set; }
    public List<Product> Products { get; set; }
}

class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

var users = new List<User>
{
    new User
    {
        Name = "Pedro",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 10,
                Products = new List<Product>
                {
                    new Product { Name = "Teléfono" },
                    new Product { Name = "Cargador" }
                }
            }
        }
    }
};

Para obtener todos los productos de todos los pedidos de todos los usuarios, usamos doble SelectMany:

var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);
// IEnumerable<Product>

Así vas abriendo primero un nivel de "cajas", luego el siguiente.

Ejemplo 3: Usando SelectMany en Query Syntax

Si te mola más la sintaxis tipo SQL de LINQ (Query Syntax), SelectMany se expresa con varios from seguidos:

var allProducts =
    from user in users
    from order in user.Orders
    from product in order.Products
    select product;

Aquí cada from siguiente coge un elemento de la colección que devuelve el anterior.


users
  └─ user1
        └─ order1 
             └─ product1, product2
        └─ order2
             └─ product3
  └─ user2
        └─ order3
             └─ product4
Esquema de colecciones anidadas para SelectMany

Query Syntax recorre todos los caminos y devuelve una sola lista larga de productos.

4. Usos y detalles

SelectMany para transformar y filtrar

En SelectMany puedes no solo desempaquetar, sino también filtrar o proyectar elementos a la vez. Por ejemplo, elegir solo productos que cuesten más de 1000 euros de todos los pedidos:

var expensiveProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products)
    .Where(p => p.Price > 1000);

O — metiendo un poco de lógica dentro de SelectMany (con un selector extra):

var expensiveProducts = users
    .SelectMany(u => u.Orders.SelectMany(o => o.Products))
    .Where(p => p.Price > 1000);

Es cuestión de gustos: ambos dividen el problema en pasos secuenciales.

Usando la sobrecarga de SelectMany con proyección

Una sobrecarga "avanzada" del método permite no solo desempaquetar, sino también formar el resultado al vuelo. Por ejemplo: obtener pares "Nombre de usuario — Nombre de producto":

var userProductPairs = users.SelectMany(
    user => user.Orders.SelectMany(order => order.Products),
    (user, product) => new { user.Name, product.Name }
);

¿Cómo va esto? El primer parámetro — user => user.Orders.SelectMany(order => order.Products) — desempaqueta todos los productos del usuario, el segundo — combina el objeto usuario original y el producto dentro del enumerador anidado.

5. Cosas importantes y errores típicos

Puede pasar que te olvides de poner SelectMany y en vez de una secuencia plana te sale un array de "cajas". Por ejemplo, cuando usas Select normal en vez de SelectMany donde deberías desempaquetar todos los elementos. Así no puedes recorrer ni procesar los elementos directamente.

Es muy fácil liarse si las colecciones están anidadas, sobre todo con muchos niveles. Hazte la costumbre: si necesitas una lista plana y no una lista de listas, usa sin miedo SelectMany.

¿Dónde te sirve en la práctica?

En cualquier tarea donde tienes un objeto-colección, y cada elemento tiene su propia colección (por ejemplo, "usuario — pedidos", "curso — estudiantes", "noticia — comentarios"), vas a volver a SelectMany. En entrevistas es uno de los temas "favoritos": no todos los que empiezan distinguen Select de SelectMany y saben desempaquetar colecciones bien.

En proyectos reales te permite hacer código bonito, limpio y rápido sin niveles de anidamiento de más. En la plataforma .NET es la forma estándar y optimizada de trabajar con colecciones "por capas".

Diferencia entre Select y SelectMany

El método Select mantiene la estructura: si lo usas en una colección de usuarios, cada uno con su lista de pedidos, el resultado es una colección de colecciones — por ejemplo, [[A, B], [C]]. Esto va bien si quieres seguir trabajando con los datos por grupos (por ejemplo, por usuario).

Pero SelectMany hace un "aplanado" — junta todas las colecciones anidadas en una sola secuencia: [A, B, C]. Esto mola cuando no te importa la agrupación y quieres trabajar con todos los elementos internos como una sola masa — por ejemplo, buscar todos los productos, sin importar a qué usuario o pedido pertenecen.


+---------------------+         +--------------------+
| users               |         | allOrders          |
+---------------------+         +--------------------+
| Iván: [A, B]        | --+   +-> Order A           |
| María: [C]          | --+ | +-> Order B           |
+---------------------+   | | +-> Order C           |
                          | | +--------------------+
     Select:              | |
   [[A, B], [C]] <--------+ +
                          |
     SelectMany: <--------+
   [A, B, C]
Esquema de la diferencia entre Select y SelectMany

En resumen:

  • Select → "dame las listas de pedidos por usuario";
  • SelectMany → "dame simplemente todos los pedidos".
2
Tarea
C# SELF, nivel 33, lección 2
Bloqueada
Uso de SelectMany para unir colecciones
Uso de SelectMany para unir colecciones
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION