1. Introducción
Cuando trabajas solo con una lista "plana" de objetos (por ejemplo, una lista de nuestros productos o usuarios de las lecciones anteriores), todo es bastante sencillo: filtramos, transformamos, ordenamos. Pero en aplicaciones reales de negocio, en entrevistas (¡y hasta en tareas de casa!) a menudo te encuentras con colecciones que dentro contienen otras colecciones.
Por ejemplo, hay una clase User que tiene una lista de pedidos. O, digamos, una clase Product que tiene una lista de reseñas. Y ahora la tarea: ¿cómo obtener la lista de todos los pedidos de todos los usuarios? ¿O juntar todas las reseñas de todos los productos? ¿Y si necesitas la lista de todos los productos en todos los pedidos?
En estas situaciones, el Select normal e incluso la magia de C# a veces no es suficiente. Aquí entra en escena el héroe — el operador SelectMany, que convierte una "lista de listas" en simplemente una "lista".
Analogía en la cocina
Si tuvieras una caja, dentro de la cual hay más cajas, y dentro de ellas hay galletas, y tu tarea es volcar todas las galletas en un bol grande. No cogerías cada caja y sacarías una galleta a la vez, sino que cogerías todas las cajas y todas las galletas — al bol.
Eso es justo lo que hace SelectMany: "aplana" la colección, convirtiendo una "caja con cajas" en un solo "bol" con el contenido.
2. Recordemos el ejemplo de la aplicación
En las lecciones anteriores creamos una aplicación sencilla para trabajar con usuarios y productos. Para esta lección añadimos colecciones de elementos "anidados".
Supongamos que tenemos las siguientes clases:
public class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
Ahora tenemos una lista de usuarios, cada uno con una lista de pedidos, y cada pedido — una lista de productos.
¿Cómo obtener todos los productos de todos los pedidos de todos los usuarios?
Supón que necesitamos hacer una lista general de todos los productos — sin anidamiento, simplemente en un "bol". ¿Qué pasa si intentamos usar el Select normal?
List<User> users = ...; // supón que ya tenemos los datos en algún sitio
var allOrders = users.Select(u => u.Orders);
El tipo de la variable allOrders será... IEnumerable<List<Order>>. Hemos obtenido una "lista de listas", es decir, un montón de colecciones de pedidos. Lo que queremos es una sola lista plana de todos los pedidos.
Podemos intentar otra vez:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
¿Y ahora? ¡Ahora tenemos una "lista de listas de listas"! O sea, solo estamos complicando la estructura, no simplificándola.
3. Solución: el operador SelectMany
Aquí es donde está toda la potencia de SelectMany. Su tarea es "aplanar" (flatten) las colecciones anidadas en una sola.
Sintaxis general:
collection.SelectMany(item => item.ColeccionInterna)
En nuestro caso:
var allOrders = users.SelectMany(u => u.Orders);
// Tipo: IEnumerable<Order>
Ahora tenemos simplemente una secuencia de todos los pedidos de todos los usuarios.
¡Vamos aún más profundo! Todos los productos de todos los pedidos de todos los usuarios:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Tipo: IEnumerable<Product>
¿Y si lo hacemos en una sola línea?
Sí, también se puede:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
Esta cadena es mejor leerla por partes, sobre todo si no eres un robot.
4. Veámoslo con un ejemplo grande
Vamos a crear datos de prueba y ver claramente cómo funciona SelectMany.
var users = new List<User>
{
new User
{
Name = "Alicia",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Galleta" },
new Product { Name = "Leche" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "Chocolate" }
}
}
}
},
new User
{
Name = "Bob",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "Café" },
new Product { Name = "Té" }
}
}
}
}
};
"Volcamos" todos los productos en un bol
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Resultado del programa:
Galleta
Leche
Chocolate
Café
Té
¿Qué pasa en cada paso?
- users.SelectMany(u => u.Orders) — cogemos todos los usuarios y "desplegamos" sus pedidos en una sola colección de pedidos (ahora tenemos 3 pedidos).
- .SelectMany(o => o.Products) — desplegamos la lista de productos de todos los pedidos.
Ojo: Si hubiéramos usado el Select normal, habríamos obtenido una colección de colecciones (por ejemplo, IEnumerable<List<Product>>). Pero con SelectMany obtenemos una colección "plana" de productos: IEnumerable<Product>.
5. ¿Cómo se ve en un esquema?
Antes de SelectMany
users (List<User>)
└── User #1
│ └── Orders (List<Order>)
│ ├── Order #1 -> Products (List<Product>)
│ └── Order #2 -> Products (List<Product>)
└── User #2
└── Orders (List<Order>)
└── Order #3 -> Products (List<Product>)
Después del primer SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
Después del segundo SelectMany(o => o.Products)
IEnumerable<Product>: [Galleta, Leche, Chocolate, Café, Té]
6. Comparación: Select y SelectMany
| Select | SelectMany | |
|---|---|---|
| Devuelve | Colección de "colecciones" | Colección plana |
| Ejemplo | → |
→ |
| Se usa | Si quieres mantener la estructura anidada | Si quieres una sola secuencia plana |
Tabla: cuándo usar cada operador
| Qué necesitas hacer | Qué usar | Resultado |
|---|---|---|
| Obtener una lista de listas (sin "aplanar") | Select | |
| Obtener una sola secuencia "plana" | SelectMany | |
| Obtener pares "padre-hijo" | SelectMany con selector de resultado | |
| Solo transformar elementos de una colección "plana" | Select | |
7. Detalles útiles
Sintaxis Query Syntax (para fans del "estilo SQL")
Para los que valoran la expresividad — LINQ permite escribir consultas también en una sintaxis parecida a SQL:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
Este código hace lo mismo que la cadena de SelectMany, solo que parece un "from" multinivel.
"Aplanando" una matriz
Un ejemplo clásico de uso de SelectMany — trabajar con un array bidimensional o una lista de listas.
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
¿Cómo obtener una sola lista de todos los números?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Sintaxis avanzada
A veces, usando la sobrecarga de SelectMany, puedes devolver no solo el elemento de la colección interna, sino también información de la externa.
Por ejemplo: necesitas obtener pares "Nombre de usuario — Nombre de producto" para cada producto en cada pedido.
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(
order => order.Products,
(order, product) => new { UserName = user.Name, ProductName = product.Name }
)
);
foreach (var pair in userProductPairs)
{
Console.WriteLine($"{pair.UserName} pidió {pair.ProductName}");
}
Aquí el segundo parámetro de la lambda es el elemento de la colección externa. Muy útil para no "perder" información de los niveles externos.
9. Escenarios prácticos
Muchos pedidos, muchos productos
Eres desarrollador de una tienda online. ¿Quieres contar cuántos productos únicos han comprado todos los clientes? Así:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Formando una colección plana desde una jerarquía
Tienes una lista de departamentos de la empresa, cada departamento — empleados. ¿Quieres obtener la lista de todos los empleados de la empresa?
var allEmployees = departments.SelectMany(d => d.Employees);
Trabajando con strings: "aplanando" un array de palabras en caracteres
string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
Console.Write(c + " "); // h e l l o w o r l d
}
10. Errores típicos y detalles
Ahora que te has inspirado con el poder de SelectMany, es el momento de avisar sobre algunos errores comunes.
El más popular — esperar que Select te dé una colección "plana". En realidad, siempre devuelve una colección de colecciones si la función interna devuelve una colección. Al final tienes que escribir bucles dobles o te lías con los tipos.
El segundo error — no darte cuenta de que "aplanar" lleva a perder información sobre el elemento externo: por ejemplo, si solo trabajas con productos, no sabrás en qué pedido o de qué usuario era, si no usas la sobrecarga con selector de resultado.
El tercer detalle — si tus listas internas pueden ser null, antes de usar SelectMany añade protección: o filtrado, o reemplazo por una lista vacía, si no, tendrás un NullReferenceException más rápido de lo que pulsas F5.
GO TO FULL VERSION