La yield
parola chiave consente di creare un IEnumerable<T>
nel modulo su un blocco iteratore . Questo blocco iteratore supporta l' esecuzione differita e se non si ha familiarità con il concetto, potrebbe apparire quasi magico. Tuttavia, alla fine della giornata, è solo il codice che viene eseguito senza strani trucchi.
Un blocco iteratore può essere descritto come zucchero sintattico in cui il compilatore genera una macchina a stati che tiene traccia di quanto è progredita l'enumerazione dell'enumerabile. Per enumerare un enumerabile, spesso si utilizza un foreach
ciclo. Tuttavia, un foreach
ciclo è anche zucchero sintattico. Quindi sei due astrazioni rimosse dal codice reale, motivo per cui inizialmente potrebbe essere difficile capire come tutto funzioni insieme.
Supponiamo di avere un blocco iteratore molto semplice:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
I blocchi di iteratori reali spesso hanno condizioni e loop ma quando si controllano le condizioni e si srotolano i loop, questi finiscono comunque come yield
istruzioni interfogliate con altro codice.
Per enumerare il blocco iteratore foreach
viene utilizzato un ciclo:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Ecco l'output (nessuna sorpresa qui):
Inizio
1
Dopo 1
2
Dopo 2
42
Fine
Come detto sopra foreach
è lo zucchero sintattico:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Nel tentativo di districare questo ho creato un diagramma di sequenza con le astrazioni rimosse:
Anche la macchina a stati generata dal compilatore implementa l'enumeratore ma per rendere più chiaro il diagramma, li ho mostrati come istanze separate. (Quando la macchina a stati viene enumerata da un altro thread in realtà si ottengono istanze separate ma qui i dettagli non sono importanti.)
Ogni volta che si chiama il blocco iteratore viene creata una nuova istanza della macchina a stati. Tuttavia, nessuno dei tuoi codici nel blocco iteratore viene eseguito fino a quando non enumerator.MoveNext()
viene eseguito per la prima volta. Ecco come funziona l'esecuzione differita. Ecco un esempio (piuttosto sciocco):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
A questo punto l'iteratore non è stato eseguito. La Where
clausola crea un nuovo IEnumerable<T>
che avvolge il IEnumerable<T>
reso da IteratorBlock
ma questo enumerabile deve ancora essere enumerato. Questo succede quando si esegue un foreach
ciclo:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Se si enumera l'enumerabile due volte, viene creata una nuova istanza della macchina a stati ogni volta e il blocco iteratore eseguirà lo stesso codice due volte.
Si noti che i metodi di LINQ piace ToList()
, ToArray()
, First()
, Count()
ecc userà un foreach
ciclo per enumerare l'enumerabile. Ad esempio ToList()
, enumera tutti gli elementi dell'enumerabile e li memorizza in un elenco. È ora possibile accedere all'elenco per ottenere tutti gli elementi dell'enumerabile senza eseguire nuovamente il blocco iteratore. C'è un compromesso tra l'uso della CPU per produrre gli elementi dell'enumerabile più volte e la memoria per memorizzare gli elementi dell'enumerazione per accedervi più volte quando si usano metodi come ToList()
.