CodeGym /Corsi /C# SELF /Allineare le collezioni con

Allineare le collezioni con SelectMany

C# SELF
Livello 31 , Lezione 4
Disponibile

1. Introduzione

Quando lavori solo con una lista "piatta" di oggetti (tipo la lista dei nostri prodotti o utenti delle lezioni precedenti), è tutto abbastanza semplice: filtri, trasformi, ordini. Ma nelle vere app business, ai colloqui (e pure nei compiti a casa!) spesso ti capitano collezioni che dentro hanno altre collezioni.

Per esempio, c'è una classe User che ha una lista di ordini. Oppure, diciamo, una classe Product che ha una lista di recensioni. E ora la sfida: come ottenere la lista di tutti gli ordini di tutti gli utenti? O raccogliere tutte le recensioni di tutti i prodotti? E se ti serve la lista di tutti i prodotti in tutti gli ordini?

In queste situazioni il solito Select e pure la magia di C# a volte non bastano. Qui entra in scena l'eroe — l'operatore SelectMany, che trasforma una "lista di liste" in una semplice "lista".

Analogia in cucina

Se avessi una scatola, dentro cui ci sono altre scatole, e in quelle scatole ci sono biscotti, e il tuo compito è versare tutti i biscotti in una ciotola grande. Non prenderesti ogni scatola e verseresti un biscotto alla volta, ma prenderesti tutte le scatole e butteresti tutti i biscotti nella ciotola in una volta sola.

Ecco, questo fa SelectMany: "appiattisce" la collezione, trasformando una "scatola con scatole" in una "ciotola" con tutto il contenuto.

2. Ricordiamo l'esempio dell'app

Nelle lezioni scorse abbiamo creato una semplice app per lavorare con utenti e prodotti. Per questa lezione aggiungiamo collezioni di elementi "annidati".

Supponiamo di avere queste classi:


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; }
}

Ora abbiamo una lista di utenti, ognuno con una lista di ordini, e ogni ordine ha una lista di prodotti.

Come ottenere tutti i prodotti di tutti gli ordini di tutti gli utenti?

Supponiamo che dobbiamo fare una lista di tutti i prodotti — senza annidamenti, solo in una "ciotola". Se proviamo a usare il solito Select, che succede?


List<User> users = ...; // supponiamo che i dati ci siano già da qualche parte

var allOrders = users.Select(u => u.Orders);

Il tipo della variabile allOrders sarà... IEnumerable<List<Order>>. Otteniamo una "lista di liste", cioè tante collezioni di ordini. Ma noi vorremmo avere una sola lista piatta di tutti gli ordini.

Proviamo ancora:


var allProducts = users.Select(u => u.Orders)
                       .Select(oList => oList.Select(o => o.Products));

E ora? Ora abbiamo una "lista di liste di liste"! Quindi stiamo solo complicando la struttura invece di semplificarla.

3. Soluzione: operatore SelectMany

Ecco dove si nasconde tutta la potenza di SelectMany. Il suo compito è "appiattire" (flatten) le collezioni annidate in una sola.

Sintassi generale:


collection.SelectMany(item => item.CollezioneInterna)

Nel nostro caso:


var allOrders = users.SelectMany(u => u.Orders);
// Tipo: IEnumerable<Order>

Ora abbiamo semplicemente la sequenza di tutti gli ordini di tutti gli utenti.

Andiamo ancora più a fondo! Tutti i prodotti di tutti gli ordini di tutti gli utenti:


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

E se tutto in una riga?

Sì, si può fare anche così:


var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));

Questa catena è meglio leggerla a pezzi, soprattutto se non sei un robot.

4. Vediamolo con un esempio grande

Creiamo dei dati di test e vediamo chiaramente come funziona SelectMany.


var users = new List<User>
{
    new User
    {
        Name = "Alisa",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 1,
                Products = new List<Product>
                {
                    new Product { Name = "Biscotto" },
                    new Product { Name = "Latte" },
                }
            },
            new Order
            {
                Id = 2,
                Products = new List<Product>
                {
                    new Product { Name = "Cioccolato" }
                }
            }
        }
    },
    new User
    {
        Name = "Bob",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 3,
                Products = new List<Product>
                {
                    new Product { Name = "Caffè" },
                    new Product { Name = "Tè" }
                }
            }
        }
    }
};

"Versiamo" tutti i prodotti in una ciotola


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);

foreach (var product in allProducts)
{
    Console.WriteLine(product.Name);
}

Risultato del programma:

Biscotto
Latte
Cioccolato
Caffè
Tè

Cosa succede ad ogni step?

  1. users.SelectMany(u => u.Orders) — prendiamo tutti gli utenti e "apriamo" i loro ordini in una sola collezione di ordini (ora abbiamo 3 ordini).
  2. .SelectMany(o => o.Products) — apriamo la lista dei prodotti da tutti gli ordini.

Attenzione: Se avessimo usato il solito Select, avremmo ottenuto una collezione di collezioni (tipo IEnumerable<List<Product>>). Ma con SelectMany otteniamo una collezione "piatta" di prodotti: IEnumerable<Product>.

5. Come appare in uno schema

Prima di 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>)

Dopo il primo SelectMany(u => u.Orders)


IEnumerable<Order>: [Order #1, Order #2, Order #3]

Dopo il secondo SelectMany(o => o.Products)


IEnumerable<Product>: [Biscotto, Latte, Cioccolato, Caffè, Tè]

6. Confronto: Select e SelectMany

Select SelectMany
Restituisce Collezione di "collezioni" Collezione piatta
Esempio
users.Select(u => u.Orders)
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
IEnumerable<Order>
Quando usarlo Se vuoi mantenere la struttura annidata Se vuoi una sola sequenza piatta

Tabella: quando usare quale operatore

Cosa vuoi fare Cosa usare Risultato
Ottenere una lista di liste (senza "appiattire") Select
IEnumerable<ICollection<T>>
Ottenere una sola sequenza "piatta" SelectMany
IEnumerable<T>
Ottenere coppie "genitore-figlio" SelectMany con selettore di risultato
IEnumerable<CustomType>
Solo trasformare gli elementi di una collezione "piatta" Select
IEnumerable<TransformedType>

7. Trucchi utili

Sintassi Query Syntax (per chi ama lo stile "SQL")

Per chi apprezza l'espressività — LINQ ti permette di scrivere query anche in una sintassi simile a SQL:


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

Questo codice fa la stessa cosa della catena di SelectMany, solo che sembra un "from" multilivello.

"Appiattire" una matrice

Un classico esempio di SelectMany — lavorare con un array bidimensionale o una lista di liste.


List<List<int>> matrix = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int> { 4, 5 },
    new List<int> { 6 }
};

Come ottenere una sola lista di tutti i numeri?


var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
    Console.Write(value + " "); // 1 2 3 4 5 6
}

8. Sintassi avanzata

A volte, usando l'overload di SelectMany, puoi restituire non solo l'elemento della collezione interna, ma anche info dalla collezione esterna.

Ecco un esempio: vuoi ottenere coppie "Nome utente — Nome prodotto" per ogni prodotto in ogni ordine.


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} ha ordinato {pair.ProductName}");
}

Qui il secondo parametro della lambda è l'elemento della collezione esterna. Molto utile per non "perdere" info dai livelli esterni.

9. Scenari pratici

Tanti ordini, tanti prodotti

Sei uno sviluppatore di un e-commerce. Vuoi contare quanti prodotti unici hanno comprato tutti i clienti? Ecco:


var uniqueProductNames = users
    .SelectMany(user => user.Orders)
    .SelectMany(order => order.Products)
    .Select(product => product.Name)
    .Distinct();

foreach (var name in uniqueProductNames)
{
    Console.WriteLine(name);
}

Creare una collezione piatta da una gerarchia

Hai una lista di reparti aziendali, ogni reparto ha dei dipendenti. Vuoi ottenere la lista di tutti i dipendenti dell'azienda:


var allEmployees = departments.SelectMany(d => d.Employees);

Lavorare con le stringhe: "appiattire" un array di parole in caratteri


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. Errori tipici e dettagli

Ora che ti sei gasato con la potenza di SelectMany, è il momento di avvisarti di alcuni errori che capitano spesso.

Il più popolare — aspettarsi che Select dia una collezione "piatta". In realtà restituisce sempre una collezione di collezioni, se la funzione interna restituisce una collezione. Alla fine ti tocca scrivere doppi cicli o ti incasini con i tipi.

Il secondo errore — non accorgersi che "appiattendo" perdi info sull'elemento esterno: per esempio, se lavori solo con i prodotti, non saprai in quale ordine o di quale utente erano, se non usi l'overload con il selettore di risultato.

Terzo dettaglio — se le tue liste interne possono essere null, prima di usare SelectMany aggiungi una protezione: o filtra, o sostituisci con una lista vuota, altrimenti becchi un NullReferenceException più veloce di quanto premi F5.

1
Sondaggio/quiz
Introduzione a LINQ, livello 31, lezione 4
Non disponibile
Introduzione a LINQ
Basi di LINQ: query semplici e filtraggio dei dati
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION