Ogni ciclo foreach e ogni query LINQ funziona attraverso una piccola interfaccia: IEnumerable<T>. Capirla spiega perché LINQ è lazy, perché alcune sequenze si possono leggere una volta sola e come yield return ti permette di scrivere sequenze tue con un semplice ciclo.
Cos'è IEnumerable
IEnumerable<T> ha un solo metodo, GetEnumerator(). L'enumeratore che restituisce ha MoveNext() (avanza, e dice se c'è un elemento) e Current (l'elemento). Un foreach è una forma abbreviata per guidare quell'enumeratore:
Output:
red green blue
red green blue
L'enumeratore parte prima del primo elemento, quindi il primo MoveNext() si sposta su di esso. Quando MoveNext() restituisce false la sequenza è finita. Il blocco using rilascia l'enumeratore, cosa importante per gli iteratori che tengono risorse come file aperti.
IEnumerable, ICollection e List a confronto
Le interfacce delle collezioni formano una scala. Ogni gradino promette di più:
| Tipo | Aggiunge | Uso tipico |
|---|---|---|
IEnumerable<T> | solo foreach | Parametri che devi solo scorrere; risultati LINQ; sequenze lazy |
IReadOnlyCollection<T> | Count | Valori di ritorno che chi chiama non deve modificare |
ICollection<T> | Count, Add, Remove, Contains | Codice che aggiunge a una collezione di qualsiasi tipo |
IReadOnlyList<T> | Accesso per indice [i] | Risultati di sola lettura con posizioni |
IList<T> | Accesso per indice, Insert, RemoveAt | Codice che modifica per posizione |
List<T> | La classe concreta che implementa tutte queste | Memorizzazione |
Un metodo che deve solo scorrere il proprio input dovrebbe accettare IEnumerable<T>, così chi chiama può passare un array, una lista, un set o una query LINQ senza convertirli. Un metodo che restituisce dati dovrebbe di solito restituire qualcosa che dica "questo è già in memoria", come List<T> o IReadOnlyList<T>, perché un valore di ritorno IEnumerable<T> potrebbe essere una query che lavora ogni volta che viene letta.
Output:
30
15
4.0
30
Scrivere un iteratore con yield return
Per produrre una sequenza tua, scrivi un metodo che restituisce IEnumerable<T> e usa yield return per ogni elemento. Il compilatore trasforma il metodo in una classe che implementa l'enumeratore al posto tuo.
Output:
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
In EvenNumbers non c'è nessuna lista. Ogni yield return consegna un valore a chi chiama e mette in pausa il metodo, mantenendo vive le sue variabili locali (qui i) finché chi chiama non chiede il valore successivo.
Valutazione lazy: attenzione all'ordine di esecuzione
Dato che il metodo si mette in pausa, il suo corpo e il ciclo di chi chiama si alternano. Stampare da entrambe le parti mostra l'alternanza:
Output:
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
Chiamare Numbers() non ha stampato "start": ha solo creato l'enumerable. Il corpo è partito al primo MoveNext() dentro il foreach, è andato avanti fino al primo yield return e si è fermato. Ogni passo successivo è ripreso dalla riga dopo lo yield. Where e Select di LINQ si comportano allo stesso modo (il runtime li implementa con classi enumeratore scritte a mano per velocità, ma l'effetto è identico), ed è per questo che le query LINQ sono differite.
yield break
yield break termina la sequenza in anticipo, la versione iteratore di return:
Output:
Subject: Invoice
From: shop@example.com
Non puoi mescolare return value; e yield return nello stesso metodo. Appena un metodo contiene yield, è un iteratore, e yield break è l'unico modo per uscirne in anticipo.
Sequenze infinite
Dato che vengono calcolati solo gli elementi richiesti, un iteratore può descrivere una sequenza senza fine. È chi consuma a decidere quanti prenderne:
Output:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
Qui while (true) è sicuro perché Take e First smettono di chiedere quando hanno ottenuto quello che serve. Chiamare ToList() o Count() su una sequenza infinita, invece, non termina mai, e nemmeno enumerare un OrderBy su di essa, perché per ordinare servono prima tutti gli elementi.
Le trappole delle sequenze lazy
I controlli sugli argomenti vengono eseguiti tardi. Tutto il corpo di un iteratore è differito, compresa la validazione all'inizio. La soluzione è dividere il metodo: un metodo normale controlla gli argomenti e restituisce il risultato di un iteratore privato.
Output:
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
Ogni enumerazione esegue di nuovo il metodo. Un iteratore non mette in cache niente. Scorrere due volte Numbers() stampa "start" due volte, rilegge il file due volte o chiama il servizio web due volte. Gli IDE lo segnalano come "possible multiple enumeration of IEnumerable". Chiama ToList() una volta quando ti servono gli elementi più di una volta.
finally viene eseguito quando chi consuma si ferma. Se chi chiama esce in anticipo dal foreach, l'enumeratore viene rilasciato e qualsiasi blocco finally nell'iteratore viene eseguito; è così che un iteratore che legge un file lo chiude:
Output:
open sensor
10
20
30
close sensor
done
Alcune regole che il compilatore impone sugli iteratori: yield return non può comparire dentro un blocco try che ha un catch (un try/finally va bene), dentro una lambda o un metodo anonimo, o in un metodo con parametri ref o out.
Stream asincroni
Per le sequenze i cui elementi arrivano in modo asincrono (pagine da un'API, righe da un database), C# 8 e successivi (con .NET Core 3.0 o successivi) combinano gli iteratori con await tramite IAsyncEnumerable<T>:
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
Chi consuma usa await foreach, e ogni elemento viene atteso man mano che viene prodotto.
Errori comuni
- Restituire
IEnumerable<T>da una query e leggerlo molte volte. Ogni lettura la riesegue; restituisci invece unaList<T>. - Validare gli argomenti in un iteratore. Il controllo è differito; dividilo in un metodo wrapper e un iteratore.
- Chiamare
Count()oToList()su un iteratore infinito. Non finisce mai; usa primaTake. - Aspettarsi che un parametro
IEnumerable<T>abbiaCounto[i]. Usa ilCount()di LINQ (che può enumerare) oppure accettaIReadOnlyList<T>.
Domande frequenti
Che cos'è IEnumerable in C#?
IEnumerable<T> è l'interfaccia di qualsiasi cosa si possa scorrere con foreach. Ha un solo metodo, GetEnumerator(), che restituisce un IEnumerator<T> con MoveNext() e Current. Array, liste, dizionari, set, query LINQ e metodi iteratori la implementano tutti.
Che differenza c'è tra IEnumerable e List in C#?
IEnumerable<T> promette solo che puoi iterare; non ha la proprietà Count, né un indicizzatore, né Add, e può essere calcolato in modo lazy ogni volta che lo scorri. List<T> è una collezione concreta memorizzata in memoria con tutte queste cose. Accetta IEnumerable<T> nei parametri quando devi solo iterare, e restituisci una List<T> o una IReadOnlyList<T> quando chi chiama deve accedere per indice o contare.
Cosa fa yield return in C#?
yield return value; dentro un metodo che restituisce IEnumerable<T> consegna un elemento a chi chiama e mette in pausa il metodo in quel punto. La volta successiva che chi chiama chiede un elemento, il metodo riprende dalla riga seguente. Il compilatore riscrive il metodo come una macchina a stati, quindi tu scrivi un ciclo e ottieni una sequenza lazy.
Cos'è yield break in C#?
yield break; termina la sequenza: il foreach di chi chiama finisce normalmente dopo gli elementi già prodotti. È l'equivalente di return per gli iteratori e si usa per fermarsi prima, per esempio quando si raggiunge un limite o l'input è vuoto.
Perché il mio metodo iteratore non lancia un'eccezione quando lo chiamo?
Il corpo di un metodo iteratore non viene eseguito finché la sequenza non viene enumerata, quindi i controlli sugli argomenti all'inizio vengono eseguiti al primo MoveNext, non al momento della chiamata. Per validare subito, metti i controlli in un normale metodo pubblico che poi chiama un metodo iteratore privato con le istruzioni yield.