CodeGym /Cursos /C# SELF /Juntando várias fontes: Se...

Juntando várias fontes: SelectMany no LINQ

C# SELF
Nível 33, Lição 2
Disponível

1. Introdução

Vamos lembrar como funciona o Select normal:

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

Pra cada usuário, pegamos o nome dele — e temos uma lista de nomes. Moleza! Mas e se a gente quiser pegar a lista de todos os pedidos de todos os usuários?

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

O que acontece aqui? Pra cada usuário, a gente pega uma lista de pedidos. Ou seja, o resultado é uma coleção de coleções (IEnumerable<List<Order>>). É tipo uma prateleira cheia de caixas: pra pegar cada pedido, você tem que abrir cada caixa (lista de pedidos do usuário) uma por uma.

A gente quer "desempacotar" tudo e trabalhar com uma lista normal de pedidos. É aí que entra o SelectMany.

Analogia: desempacotando caixas

Imagina que todas as suas coisas não ficam jogadas, mas sim em caixas. Cada caixa é uma coleção, tipo as compras de um usuário. Se você quiser ver TODAS as coisas de uma vez (tipo fazer um inventário geral), não vai ficar abrindo caixa por caixa, olhando dentro, e depois de novo e de novo. Muito mais fácil jogar tudo de todas as caixas numa pilha só — e aí sim olhar o que tem. É assim que o SelectMany funciona.

2. O que é o SelectMany?

SelectMany é um método do LINQ que pega uma coleção de coleções e transforma tudo em uma coleção "plana". "Plana" aqui quer dizer que o resultado não tem mais listas internas — todos os itens das coleções aninhadas ficam no mesmo nível, como se tivessem sido "puxados" pra fora. Nada de boneca russa, só uma lista grandona de valores.

A assinatura do método é assim:


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

É simples: você tem uma coleção (tipo uma lista de usuários), e cada elemento tem dentro outra coleção (tipo a lista de pedidos desse usuário). O SelectMany pega cada usuário, pega os pedidos dele e junta tudo num fluxo só de pedidos — sem "embalagem", sem aninhamento. É perfeito pra quando você quer transformar um "lista de listas" em uma lista normal.

3. Exemplos simples

Exemplo 1: Lista de usuários e seus pedidos

Vamos começar com o nosso modelo de app, que a gente já vem usando:

// Supondo que essas classes já existem das aulas 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; }
}

Imagina que temos uma lista de usuários:

var users = new List<User>
{
    new User
    {
        Name = "Ivan",
        Orders = new List<Order>
        {
            new Order { Id = 1, ProductName = "Livro" },
            new Order { Id = 2, ProductName = "Caneta" }
        }
    },
    new User
    {
        Name = "Maria",
        Orders = new List<Order>
        {
            new Order { Id = 3, ProductName = "Notebook" }
        }
    }
};

Desafio: pegar a lista de todos os produtos que já foram comprados por todos os usuários.

Select normal:

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

Aqui, no final, temos várias "caixas", cada uma com a lista de pedidos do usuário.

SelectMany:

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

Agora a gente tem uma coleção "grandona" com todos os pedidos!

Visualização

Usuário Pedidos
Ivan Livro, Caneta
Maria Notebook
  • Depois do Select: [[Livro, Caneta], [Notebook]] — lista de listas
  • Depois do SelectMany: [Livro, Caneta, Notebook] — lista única

Exemplo 2: Trabalhando com vários níveis de aninhamento

Se, por exemplo, um pedido tem uma lista de produtos (carrinho), aí dá pra usar o SelectMany várias vezes — tipo boneca russa mesmo!

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 = "Celular" },
                    new Product { Name = "Carregador" }
                }
            }
        }
    }
};

Pra pegar todos os produtos de todos os pedidos de todos os usuários, usa o SelectMany duas vezes:

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

Assim, você abre primeiro um nível de "caixas", depois o próximo.

Exemplo 3: Usando SelectMany com Query Syntax

Se você curte mais o estilo SQL do LINQ (Query Syntax), o SelectMany vira vários from seguidos:

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

Aqui, cada from pega um item da coleção retornada pelo anterior.


users
  └─ user1
        └─ order1 
             └─ product1, product2
        └─ order2
             └─ product3
  └─ user2
        └─ order3
             └─ product4
Esquema de coleções aninhadas para o SelectMany

O Query Syntax percorre todos os caminhos e retorna uma lista grandona de produtos.

4. Usos e detalhes

SelectMany pra transformar e filtrar

No SelectMany dá pra não só abrir as coleções, mas também filtrar ou projetar os itens ao mesmo tempo. Por exemplo, pegar só produtos com preço maior que 1000 euros de todos os pedidos:

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

Ou — colocando um pouco de lógica direto dentro do SelectMany (com um seletor extra):

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

É questão de gosto: os dois jeitos dividem o problema em passos separados.

Usando a sobrecarga do SelectMany com projeção

Uma sobrecarga "avançada" do método deixa você não só abrir as coleções, mas já montar o resultado. Por exemplo: pegar pares "Nome do usuário — Nome do produto":

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

Como funciona? O primeiro parâmetro — user => user.Orders.SelectMany(order => order.Products) — abre todos os produtos do usuário, o segundo — combina o usuário original e o produto dentro do enumerador aninhado.

5. Pontos importantes e erros comuns

Pode acontecer de você esquecer de usar o SelectMany e acabar com um array de "caixas" em vez de uma sequência plana. Tipo, quando você usa o Select normal, mas precisava abrir todos os itens de uma vez. Aí não dá pra percorrer ou processar os itens direto.

É fácil se enrolar quando as coleções são aninhadas, principalmente se tem muitos níveis. Fica a dica: se você quer uma lista plana, e não uma lista de listas, pode usar o SelectMany sem medo.

Onde usar na prática?

Em qualquer situação onde você tem um objeto-coleção, e cada item tem sua própria coleção (tipo "usuário — pedidos", "curso — alunos", "notícia — comentários"), você vai acabar usando o SelectMany. Em entrevistas, esse é um dos assuntos "queridinhos": nem todo iniciante sabe a diferença entre Select e SelectMany e consegue abrir as coleções direito.

Em projetos reais, isso deixa o código bonito, direto e rápido, sem aquele monte de níveis de aninhamento. No .NET, é o jeito padrão e otimizado pra lidar com coleções "em camadas".

Diferença entre Select e SelectMany

O método Select mantém a estrutura: se você usa ele numa coleção de usuários, cada um com uma lista de pedidos, o resultado é uma coleção de coleções — tipo [[A, B], [C]]. Isso é bom se você quer trabalhar com os dados por grupo (tipo por usuário).

Já o SelectMany faz um "achatamento" — junta todas as coleções internas numa sequência só: [A, B, C]. Isso é útil quando você não liga pra agrupamento e quer trabalhar com todos os itens internos como uma massa só — tipo achar todos os produtos, não importa de qual usuário ou pedido.


+---------------------+         +--------------------+
| users               |         | allOrders          |
+---------------------+         +--------------------+
| Ivan: [A, B]        | --+   +-> Order A           |
| Maria: [C]          | --+ | +-> Order B           |
+---------------------+   | | +-> Order C           |
                          | | +--------------------+
     Select:              | |
   [[A, B], [C]] <--------+ +
                          |
     SelectMany: <--------+
   [A, B, C]
Esquema da diferença entre Select e SelectMany

Resumindo:

  • Select → "me dá as listas de pedidos por usuário";
  • SelectMany → "me dá todos os pedidos de uma vez".
2
Tarefa
C# SELF, nível 33, lição 2
Bloqueado
Usando SelectMany para unir coleções
Usando SelectMany para unir coleções
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION