CodeGym /Corsi /C# SELF /Unire più sorgenti: Select...

Unire più sorgenti: SelectMany in LINQ

C# SELF
Livello 33, Lezione 2
Disponibile

1. Introduzione

Ricordiamo come funziona il classico Select:

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

Ad ogni utente associ il suo nome — ottieni una lista di nomi. Facile! Ma se vogliamo ottenere la lista di tutti gli ordini di tutti gli utenti?

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

Cosa succede qui? Per ogni utente otteniamo una lista di ordini. Quindi il risultato sarà una collezione di collezioni (IEnumerable<List<Order>>). È come uno scaffale con delle scatole: per arrivare a ogni ordine, devi aprire manualmente ogni scatola (lista di ordini dell’utente) una per una.

Noi invece vogliamo "spacchettare" tutto il mucchio, per lavorare con esso come con una normale lista di ordini. Ed è qui che SelectMany ci salva la vita.

Analogia: spacchettare le scatole

Immagina che tutte le tue cose non siano mischiate, ma conservate in scatole. Ogni scatola è una collezione, tipo gli acquisti di un utente. Se vuoi vedere TUTTE le cose subito (tipo per un inventario globale), non ti metti a guardare scatola per scatola e dentro ogni scatola le cose, e poi di nuovo e di nuovo. Molto meglio rovesciare il contenuto di tutte le scatole in un unico mucchio — e poi guardare lì dentro. Ecco, così funziona SelectMany.

2. Cos’è SelectMany?

SelectMany è un metodo LINQ che prende una collezione di collezioni e la trasforma in una collezione "piatta". Per "piatta" qui si intende che la sequenza risultante non avrà più liste interne — tutti gli elementi delle collezioni annidate saranno sullo stesso livello, come se li avessi "tirati fuori". Niente matrioske, solo una lunga catena di valori.

La firma del metodo è così:


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

Funziona in modo semplice: hai una collezione (tipo una lista di utenti), e ogni elemento contiene dentro un’altra collezione (tipo la lista degli ordini di quell’utente). SelectMany prende ogni utente, tira fuori i suoi ordini e li unisce in un unico flusso di ordini — senza "involucri", senza annidamenti. Ed è proprio questo che lo rende comodo quando vuoi passare da una "lista di liste" a una lista normale.

3. Esempi semplici

Esempio 1: Lista di utenti e i loro ordini

Partiamo dal nostro modello di app, che stiamo sviluppando passo passo:

// Supponiamo che queste classi ci siano già dalle lezioni precedenti
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; }
}

Supponiamo di avere una lista di utenti:

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

Obiettivo: ottenere la lista di tutti i prodotti che sono stati acquistati da tutti gli utenti.

Il classico Select:

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

Qui otteniamo in uscita tante "scatole", in ognuna la lista degli ordini per utente.

SelectMany:

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

Ora otteniamo una sola "lunga" collezione di tutti gli ordini!

Visualizzazione

Utente Ordini
Ivan Libro, Penna
Maria Notebook
  • Dopo Select: [[Libro, Penna], [Notebook]] — lista di liste
  • Dopo SelectMany: [Libro, Penna, Notebook] — lista unica

Esempio 2: Lavorare con più livelli di annidamento

Se ad esempio ogni ordine ha una lista di prodotti (carrello), allora SelectMany si può usare più volte — proprio come una matrioska!

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 = "Pietro",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 10,
                Products = new List<Product>
                {
                    new Product { Name = "Telefono" },
                    new Product { Name = "Caricabatterie" }
                }
            }
        }
    }
};

Per ottenere tutti i prodotti di tutti gli ordini di tutti gli utenti, usiamo due volte SelectMany:

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

Così spacchetti prima un livello di "scatole", poi il successivo.

Esempio 3: Usare SelectMany in Query Syntax

Se preferisci la sintassi LINQ simile a SQL (Query Syntax), SelectMany si esprime con più from di fila:

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

Qui ogni from successivo prende un elemento dalla collezione restituita dal precedente.


users
  └─ user1
        └─ order1 
             └─ product1, product2
        └─ order2
             └─ product3
  └─ user2
        └─ order3
             └─ product4
Schema delle collezioni annidate per SelectMany

Query Syntax passa su tutti i percorsi e restituisce una sola lunga lista di prodotti.

4. Varianti d’uso e particolarità

SelectMany per trasformazione e filtro

In SelectMany puoi non solo spacchettare, ma anche filtrare o proiettare elementi. Ad esempio, selezionare solo i prodotti con prezzo sopra 1000 euro da tutti gli ordini:

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

Oppure — con un po’ di logica direttamente dentro SelectMany (usando un selettore aggiuntivo):

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

Questione di gusti: entrambi i modi dividono il problema in passi separati.

Usare l’overload di SelectMany con proiezione

Un overload "avanzato" del metodo permette non solo di spacchettare, ma anche di formare subito il risultato. Per esempio: ottenere le coppie "Nome utente — Nome prodotto":

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

Come funziona? Il primo parametro — user => user.Orders.SelectMany(order => order.Products) — spacchetta tutti i prodotti dell’utente, il secondo — combina l’oggetto utente originale e il prodotto dentro l’enumerator annidato.

5. Cose importanti ed errori comuni

Può capitare di dimenticare di aggiungere SelectMany e invece di una sequenza piatta ottenere un array di "scatole". Ad esempio, quando usi il classico Select invece di SelectMany dove serve spacchettare subito tutti gli elementi. Così non puoi scorrere o lavorare direttamente sugli elementi.

È facile confondersi se le collezioni sono annidate, soprattutto con molti livelli. Fai una regola: se vuoi in uscita non una lista di liste, ma una lista piatta, — usa senza paura SelectMany.

Dove ti serve nella pratica?

In qualsiasi situazione in cui hai un oggetto-collezione, e ogni elemento ha la sua collezione (tipo "utente — ordini", "corso — studenti", "notizia — commenti"), tornerai a SelectMany. Ai colloqui è uno degli argomenti "preferiti": non tutti i principianti distinguono Select da SelectMany e sanno spacchettare correttamente le collezioni.

Nei progetti reali ti permette di scrivere codice bello, conciso e veloce senza livelli inutili di annidamento. Sulla piattaforma .NET è la strada standard e ottimizzata per gestire collezioni "a strati".

Differenza tra Select e SelectMany

Il metodo Select mantiene la struttura: se lo applichi a una collezione di utenti, ognuno con una lista di ordini, il risultato sarà una collezione di collezioni — tipo [[A, B], [C]]. Utile se vuoi lavorare ancora per gruppi (ad esempio per utente).

Invece SelectMany fa il "flatten" — unisce tutte le collezioni annidate in una sola sequenza: [A, B, C]. Utile quando non ti interessa il raggruppamento, e vuoi lavorare con tutti gli elementi interni come una massa unica — tipo trovare tutti i prodotti, a prescindere da quale utente o ordine arrivano.


+---------------------+         +--------------------+
| users               |         | allOrders          |
+---------------------+         +--------------------+
| Ivan: [A, B]        | --+   +-> Order A           |
| Maria: [C]          | --+ | +-> Order B           |
+---------------------+   | | +-> Order C           |
                          | | +--------------------+
     Select:              | |
   [[A, B], [C]] <--------+ +
                          |
     SelectMany: <--------+
   [A, B, C]
Schema della differenza tra Select e SelectMany

In parole povere:

  • Select → "dammi le liste di ordini per utente";
  • SelectMany → "dammi semplicemente tutti gli ordini".
2
Compito
C# SELF, livello 33, lezione 2
Bloccato
Utilizzo di SelectMany per unire collezioni
Utilizzo di SelectMany per unire collezioni
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION