1. Introdução
Quando você trabalha só com uma lista "plana" de objetos (tipo uma lista dos nossos produtos ou usuários das aulas anteriores), é bem tranquilo: filtra, transforma, ordena. Mas em apps de verdade, em entrevistas (e até nos exercícios de casa!) aparecem coleções que têm outras coleções dentro delas.
Por exemplo, tem a classe User, que tem uma lista de pedidos. Ou, sei lá, a classe Product, que tem uma lista de avaliações. Agora pensa: como pegar a lista de todos os pedidos de todos os usuários? Ou juntar todas as avaliações de todos os produtos? E se precisar pegar a lista de todos os produtos em todos os pedidos?
Nessas situações, só o Select normal e até a mágica do C# às vezes não dão conta. É aí que entra o herói — o operador SelectMany, que transforma uma "lista de listas" em só uma "lista".
Uma analogia na cozinha
Imagina que você tem uma caixa, dentro dela tem outras caixas, e dentro delas tem biscoitos. Sua missão: jogar todos os biscoitos numa tigela grandona. Você não ia pegar cada caixa e tirar um biscoito de cada vez, né? Ia logo pegar todas as caixas e despejar tudo na tigela.
É isso que o SelectMany faz: ele "achata" a coleção, transformando uma "caixa com caixas" em uma "tigela" com tudo dentro.
2. Relembrando o exemplo do app
Nas aulas passadas, a gente criou um app simples pra trabalhar com usuários e produtos. Pra essa aula, vamos adicionar coleções de elementos "aninhados".
Digamos que temos essas classes:
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; }
}
Agora temos uma lista de usuários, cada um com uma lista de pedidos, e cada pedido com uma lista de produtos.
Como pegar todos os produtos de todos os pedidos de todos os usuários?
Digamos que a gente quer montar uma lista geral de todos os produtos — sem aninhamento, tudo numa "tigela" só. Se a gente tentar usar o Select normal, o que acontece?
List<User> users = ...; // imagina que já temos os dados em algum lugar
var allOrders = users.Select(u => u.Orders);
O tipo da variável allOrders vai ser... IEnumerable<List<Order>>. Ou seja, uma "lista de listas", várias coleções de pedidos. O que a gente queria mesmo era uma lista plana de todos os pedidos.
Dá pra tentar de novo:
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
E agora? Agora temos uma "lista de listas de listas"! Ou seja, só complicamos a estrutura, não simplificamos.
3. A solução: operador SelectMany
É aí que mora toda a força do SelectMany. O objetivo dele é "achatar" (flatten) coleções aninhadas em uma só.
Sintaxe geral:
collection.SelectMany(item => item.ColecaoInterna)
No nosso caso:
var allOrders = users.SelectMany(u => u.Orders);
// Tipo: IEnumerable<Order>
Agora temos só uma sequência de todos os pedidos de todos os usuários.
Vamos mais fundo! Todos os produtos de todos os pedidos de todos os usuários:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Tipo: IEnumerable<Product>
E se for tudo numa linha só?
Dá pra fazer assim também:
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
Essa cadeia é melhor ler por partes, principalmente se você não for um robô.
4. Vamos ver um exemplo grandão
Bora criar uns dados de teste e ver na prática como o SelectMany funciona.
var users = new List<User>
{
new User
{
Name = "Alice",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Biscoito" },
new Product { Name = "Leite" },
}
},
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 = "Chá" }
}
}
}
}
};
"Jogando" todos os produtos numa tigela só
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Resultado do programa:
Biscoito
Leite
Chocolate
Café
Chá
O que rola em cada etapa?
- users.SelectMany(u => u.Orders) — pega todos os usuários e "desdobra" os pedidos deles numa coleção só de pedidos (agora temos 3 pedidos).
- .SelectMany(o => o.Products) — desdobra a lista de produtos de todos os pedidos.
Atenção: Se a gente usasse o Select normal, ia acabar com uma coleção de coleções (tipo IEnumerable<List<Product>>). Com o SelectMany a gente pega uma coleção "plana" de produtos: IEnumerable<Product>.
5. Como fica no esquema
Antes do 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>)
Depois do primeiro SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
Depois do segundo SelectMany(o => o.Products)
IEnumerable<Product>: [Biscoito, Leite, Chocolate, Café, Chá]
6. Comparando: Select e SelectMany
| Select | SelectMany | |
|---|---|---|
| Retorna | Coleção de "coleções" | Coleção plana |
| Exemplo | → |
→ |
| Usa quando | Quer manter a estrutura aninhada | Quer uma sequência plana só |
Tabela: quando usar cada operador
| O que precisa fazer | O que usar | Resultado |
|---|---|---|
| Pegar lista de listas (não "achatar") | Select | |
| Pegar uma sequência "plana" só | SelectMany | |
| Pegar pares "pai-filho" | SelectMany com seletor de resultado | |
| Só transformar elementos de uma coleção "plana" | Select | |
7. Dicas e detalhes úteis
Sintaxe Query Syntax (pra quem curte "jeito SQL")
Pra quem gosta de expressividade — o LINQ deixa você escrever consultas num jeitão parecido com SQL:
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
Esse código faz a mesma coisa que a cadeia de SelectMany, só que parece um "from" em vários níveis.
"Achatando" uma matriz
Um exemplo clássico de SelectMany é trabalhar com array 2D ou 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 }
};
Como pegar uma lista só com todos os números?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Sintaxe avançada
Às vezes, usando uma sobrecarga do SelectMany, dá pra retornar não só o elemento da coleção interna, mas também info da externa.
Exemplo: pegar pares "Nome do usuário — Nome do produto" pra cada produto em 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} pediu {pair.ProductName}");
}
Aqui o segundo parâmetro da lambda é o elemento da coleção externa. Muito útil pra não "perder" info dos níveis de fora.
9. Cenários práticos
Muitos pedidos, muitos produtos
Você é dev de um e-commerce. Quer contar quantos produtos únicos todos os clientes compraram? Olha só:
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Montando uma coleção plana de uma hierarquia
Você tem uma lista de departamentos da empresa, cada um com funcionários. Quer pegar a lista de todos os funcionários da empresa:
var allEmployees = departments.SelectMany(d => d.Employees);
Trabalhando com strings: "achatando" um array de palavras em 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. Erros comuns e detalhes
Agora que você já tá animado com o poder do SelectMany, bora avisar de alguns erros que rolam bastante.
O mais comum é achar que o Select vai te dar uma coleção "plana". Na real, ele sempre retorna coleção de coleções se a função interna retorna uma coleção. Aí você acaba escrevendo dois loops ou se enrolando nos tipos.
Outro erro é não perceber que "achatar" faz você perder info do elemento externo: tipo, se você trabalha só com produtos, não vai saber de qual pedido ou usuário ele veio, a não ser que use a sobrecarga com seletor de resultado.
Mais um detalhe — se suas listas internas podem ser null, antes de usar o SelectMany coloca uma proteção: filtra ou troca por lista vazia, senão vai tomar um NullReferenceException mais rápido do que apertar F5.
GO TO FULL VERSION