CodeGym /Corsi /C# SELF /Esecuzione differita

Esecuzione differita

C# SELF
Livello 34, Lezione 0
Disponibile

1. Introduzione

Immagina: hai scritto una query LINQ lunga e bella e pensi — "Voilà, ora si elabora tutto!" E poi noti che non succede nulla finché non conti gli elementi o non trasformi il risultato in un array. Non è un bug, è una feature.

In .NET LINQ usa esecuzione differita: la query non parte finché non inizi davvero a estrarre i dati. È come un cameriere pigro — non corre in cucina finché non sente: "Porta il cibo!", anche se l'ordine è già stato preso.

Questo comportamento, noto come valutazione pigra (Lazy Evaluation), rende LINQ super efficiente quando lavori con sorgenti di dati grandi, potenzialmente infinite o pesanti.

Esecuzione differita vuol dire che la query LINQ non viene eseguita subito dopo che l'hai definita. Inizia a lavorare solo quando davvero inizi a scorrere o guardare i dati nella collezione.

Esempio illustrativo


var numeri = new List<int> { 1, 2, 3, 4, 5 };

// Query LINQ
var query = numeri.Where(n =>{
    Console.WriteLine($"Controlliamo {n}");
    return n % 2 == 0;
});
Console.WriteLine("Query definita, ma i numeri non sono stati controllati!");

// Solo ora inizia il ciclo!
foreach (var n in query)
{
    Console.WriteLine($"Trovato pari: {n}");
}

Cosa succede?
Quando esegui questo codice, vedrai che prima del ciclo foreach non viene stampato nulla — nemmeno le condizioni nel tuo Where vengono valutate. Appena inizi a scorrere gli elementi (tipo con foreach), la query parte.

Questo è proprio deferred execution — finché non chiedi, nessuno fa nulla!

2. Perché serve l'esecuzione differita?

L'esecuzione differita rende il codice non solo elegante, ma anche super efficiente. Perché fare un lavoro in anticipo se magari il risultato non serve nemmeno? È fondamentale quando hai a che fare con collezioni grandi o stream di dati che stanno ancora arrivando — pensa a una sorgente che può essere infinita. Caricare tutto in memoria subito sarebbe una follia.

In più, l'esecuzione differita ti permette di combinare e costruire query senza paura che vengano eseguite subito. Tutto succede solo quando inizi davvero a prendere i dati — e non un secondo prima.

Analogia

L'esecuzione differita è come la lista della spesa sul telefono: puoi aggiungere e modificare prodotti quanto vuoi, ma vai al supermercato solo quando sei davvero pronto a comprare (e solo allora usi la lista).

3. Come funziona sotto il cofano?

Le query LINQ che restituiscono IEnumerable<T> di solito sono implementate con iteratori (yield return) o costrutti pigri speciali. Ogni volta che inizi a scorrere la collezione (tipo chiamando foreach o ToList()), la query parte da capo.

Osservazione importante

Se cambi la collezione di origine tra la definizione della query e la sua esecuzione, anche i dati nuovi o modificati finiranno nel risultato.

Esempio:


var numeri = new List<int> { 1, 2, 3 };
var query = numeri.Where(n => n > 1);
numeri.Add(4); // aggiunto un nuovo numero
foreach (var n in query)
{
    Console.WriteLine(n); // stampa 2, 3, 4
}

Schema: quando viene eseguita una query LINQ?


flowchart TD
    A[Definizione della query LINQ] --> B{Esecuzione della query?}
    B -- No --> C[In attesa]
    B -- Sì (tipo foreach, ToList) --> D[Esecuzione della query]
    D --> E[Risultato o azione]
Schema della deferred execution: la query "aspetta" finché non viene davvero usata

4. Esempi dove la "pigrizia" di LINQ conta

Filtraggio pigro


var numeriGrandi = Enumerable.Range(1, 1_000_000_000)
    .Where(n => n % 123_456 == 0);

foreach (var n in numeriGrandi.Take(5))
{
    Console.WriteLine(n);
}

Cosa succede qui?
La query crea una collezione potenzialmente da un miliardo di elementi, ma in realtà filtra e restituisce solo 5 numeri! Tutto il resto non viene nemmeno calcolato e non occupa memoria.

Query annidate e transazioni

Supponiamo di avere una lista di ordini e prodotti, e vogliamo i primi 5 ordini che contengono un certo prodotto.


var ordini = GetBigOrderList(); // immaginiamo che qui ci siano migliaia di ordini

var filtrati = ordini
    .Where(order => order.Products.Any(p => p.Name == "Caffè"))
    .Take(5);

foreach(var o in filtrati)
{
    Console.WriteLine(o.Id);
}

LINQ non guarda gli altri ordini dopo aver trovato 5 corrispondenze!

5. Sorprese

Situazioni in cui l'esecuzione differita può portare a sorprese:

Modifica della sorgente dati dopo la definizione della query

Come visto prima, se tra la definizione e l'esecuzione della query la collezione di origine cambia, la query vedrà anche i nuovi dati.

Scorrere più volte la stessa query

La query LINQ viene eseguita da capo ogni volta che la enumeri.


var query = numeri.Where(n => {
    Console.WriteLine($"Controlliamo {n}");
    return n % 2 == 0;
});

foreach(var n in query) {} // primo giro
foreach(var n in query) {} // secondo giro — tutto ricalcolato da capo

Se vuoi un risultato ripetibile — materializza (fai .ToList() o .ToArray()).

6. Quando le query LINQ NON sono differite?

Non tutte le operazioni LINQ sono differite. Alcuni metodi fanno un'esecuzione immediata ("greedy") (Immediate Execution). Per esempio:

  • .ToList()
  • .ToArray()
  • .Count()
  • .Average()
  • .Sum()
  • .First(), .Last(), .Single()

Tutti questi costringono LINQ a eseguire subito la query, perché restituiscono non un IEnumerable, ma un risultato pronto.

Esempio:


var query = numeri.Where(n => n > 2);
var risultato = query.ToList(); // Qui la query viene eseguita subito!

7. Il concetto di "valutazione pigra" (Lazy Evaluation)

L'esecuzione differita (deferred execution) è un caso specifico di valutazione pigra (Lazy Evaluation) in .NET.

Valutazione pigra vuol dire che il risultato non viene calcolato finché non serve davvero. Oltre a LINQ, in C# ci sono altri meccanismi per la valutazione pigra.

Classe Lazy<T>

C# offre un tipo speciale Lazy<T> per creare valori "on demand".

Esempio semplice:


// Creiamo un numero pigro che verrà calcolato solo quando lo chiediamo
var valorePigro = new Lazy<int>(() =>
{
    Console.WriteLine("Calcolo il valore!");
    return 42;
});

Console.WriteLine("Oggetto Lazy creato, ma il valore non è stato calcolato");
Console.WriteLine($"Valore: {valorePigro.Value}"); // qui avviene il calcolo

Perché tutto questo?
Per esempio, tieni un parametro che serve raramente e il suo calcolo è lungo o costoso.

8. Tabella: metodi LINQ e modalità di esecuzione

Metodo Esecuzione differita Esecuzione immediata
Where
✅ ❌
Select
✅ ❌
Take
✅ ❌
Skip
✅ ❌
Join
✅ ❌
ToList
❌ ✅
ToArray
❌ ✅
Count
❌ ✅
First
❌ ✅

9. Errori tipici e particolarità di implementazione

"Lavoro extra" per cicli ripetuti

Dato che l'esecuzione differita fa nuovi calcoli ogni volta che cicli, a volte puoi avere ricalcoli inutili.


var queryCostosa = bigList.Where(x => CalcoloPesante(x));

var risultato1 = queryCostosa.ToList(); // calcolato — una volta
var risultato2 = queryCostosa.ToList(); // calcolato — due volte (stesso risultato, ma tempo sprecato)

Soluzione: materializza i dati una volta sola:
var cache = queryCostosa.ToList();

"Cattura l'errore se ci riesci"

Se una funzione nella query lancia un'eccezione, questa succede solo quando cicli la collezione, non quando definisci la query.

Modificare la collezione durante il ciclo

Cambiare la collezione mentre la stai scorrendo può causare l'eccezione InvalidOperationException.

2
Compito
C# SELF, livello 34, lezione 0
Bloccato
Esecuzione differita e query multiple
Esecuzione differita e query multiple
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION