CodeGym /Cursos /C# SELF /Alinhando coleções com Sel...

Alinhando coleções com SelectMany

C# SELF
Nível 31, Lição 4
Disponível

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?

  1. 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).
  2. .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
users.Select(u => u.Orders)
→
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
→
IEnumerable<Order>
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
IEnumerable<ICollection<T>>
Pegar uma sequência "plana" só SelectMany
IEnumerable<T>
Pegar pares "pai-filho" SelectMany com seletor de resultado
IEnumerable<CustomType>
Só transformar elementos de uma coleção "plana" Select
IEnumerable<TransformedType>

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.

2
Tarefa
C# SELF, nível 31, lição 4
Bloqueado
Extraindo todos os itens de listas aninhadas
Extraindo todos os itens de listas aninhadas
1
Pesquisa/teste
Introdução ao LINQ, nível 31, lição 4
Indisponível
Introdução ao LINQ
Noções básicas de LINQ: consultas simples e filtragem de dados
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION