La yield
palabra clave le permite crear un IEnumerable<T>
formulario en un bloque iterador . Este bloque iterador admite la ejecución diferida y, si no está familiarizado con el concepto, puede parecer casi mágico. Sin embargo, al final del día, solo el código se ejecuta sin ningún truco extraño.
Un bloque iterador puede describirse como azúcar sintáctico en el que el compilador genera una máquina de estados que realiza un seguimiento de hasta qué punto ha progresado la enumeración de lo enumerable. Para enumerar un enumerable, a menudo usa un foreach
bucle. Sin embargo, un foreach
bucle también es azúcar sintáctico. Entonces, son dos abstracciones eliminadas del código real, por lo que inicialmente podría ser difícil entender cómo funciona todo junto.
Suponga que tiene un bloque iterador muy simple:
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");
}
Los bloques iteradores reales a menudo tienen condiciones y bucles, pero cuando verifica las condiciones y desenrolla los bucles, terminan siendo yield
declaraciones intercaladas con otro código.
Para enumerar el iterador, bloquee un foreach
se usa bucle:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Aquí está la salida (no hay sorpresas aquí):
Empezar
1
Después de 1
2
Después de 2
42
Final
Como se indicó anteriormente foreach
es el azúcar sintáctico:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
En un intento de desenredar esto, he creado un diagrama de secuencia con las abstracciones eliminadas:
La máquina de estado generada por el compilador también implementa el enumerador, pero para que el diagrama sea más claro, los he mostrado como instancias separadas. (Cuando la máquina de estado se enumera desde otro subproceso, en realidad se obtienen instancias separadas, pero ese detalle no es importante aquí).
Cada vez que llama a su bloque iterador, se crea una nueva instancia de la máquina de estado. Sin embargo, ninguno de sus códigos en el bloque iterador se ejecuta hasta que se enumerator.MoveNext()
ejecute por primera vez. Así es como funciona la ejecución diferida. Aquí hay un ejemplo (bastante tonto):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
En este punto, el iterador no se ha ejecutado. La Where
cláusula crea un nuevo IEnumerable<T>
que envuelve el IEnumerable<T>
devuelto por IteratorBlock
pero este enumerable aún no se ha enumerado. Esto sucede cuando ejecuta un foreach
bucle:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Si enumera el enumerable dos veces, se crea una nueva instancia de la máquina de estado cada vez y su bloque iterador ejecutará el mismo código dos veces.
Tenga en cuenta que los métodos de LINQ gusta ToList()
, ToArray()
, First()
, Count()
etc. va a utilizar un foreach
bucle para enumerar la enumerable. Por ejemplo ToList()
, enumerará todos los elementos enumerables y los almacenará en una lista. Ahora puede acceder a la lista para obtener todos los elementos del enumerable sin que el bloque iterador se ejecute nuevamente. Existe una compensación entre el uso de CPU para producir los elementos de los enumerables múltiples veces y la memoria para almacenar los elementos de la enumeración para acceder a ellos varias veces cuando se utilizan métodos como ToList()
.