A yieldpalavra-chave permite criar um IEnumerable<T>no formulário em um bloco iterador . Esse bloco iterador suporta execução adiada e, se você não estiver familiarizado com o conceito, pode parecer quase mágico. No entanto, no final do dia, é apenas um código que é executado sem truques estranhos.
Um bloco iterador pode ser descrito como açúcar sintático, em que o compilador gera uma máquina de estado que controla até que ponto a enumeração do enumerável progrediu. Para enumerar um enumerável, você costuma usar um foreachloop. No entanto, um foreachloop também é açúcar sintático. Portanto, você tem duas abstrações removidas do código real, e é por isso que inicialmente pode ser difícil entender como tudo funciona em conjunto.
Suponha que você tenha um bloco iterador muito simples:
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");
}
Os blocos iteradores reais geralmente têm condições e loops, mas quando você verifica as condições e desenrola os loops, eles ainda acabam como yieldinstruções intercaladas com outro código.
Para enumerar o bloco iterador, um foreachloop é usado:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Aqui está a saída (sem surpresas aqui):
Início
1
Após 1
2
Após 2
42.
Fim
Como afirmado acima, o foreachaçúcar sintático:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Em uma tentativa de desembaraçar isso, criei um diagrama de sequência com as abstrações removidas:

A máquina de estado gerada pelo compilador também implementa o enumerador, mas para tornar o diagrama mais claro, eu os mostrei como instâncias separadas. (Quando a máquina de estado é enumerada de outro encadeamento, você obtém instâncias separadas, mas esse detalhe não é importante aqui.)
Toda vez que você chama seu bloco iterador, uma nova instância da máquina de estado é criada. No entanto, nenhum dos seus códigos no bloco iterador é executado até ser enumerator.MoveNext()executado pela primeira vez. É assim que a execução adiada funciona. Aqui está um exemplo (bastante bobo):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
Neste ponto, o iterador não foi executado. A Wherecláusula cria um novo IEnumerable<T>que agrupa o IEnumerable<T>retornado por, IteratorBlockmas esse enumerável ainda não foi enumerado. Isso acontece quando você executa um foreachloop:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Se você enumerar o enumerável duas vezes, uma nova instância da máquina de estado será criada a cada vez e seu bloco iterador executará o mesmo código duas vezes.
Note-se que os métodos LINQ gosto ToList(), ToArray(), First(), Count()etc vai usar um foreachcircuito para enumerar o enumeráveis. Por exemplo ToList(), enumerará todos os elementos do enumerável e os armazenará em uma lista. Agora você pode acessar a lista para obter todos os elementos do enumerável sem que o bloco iterador seja executado novamente. Existe uma troca entre usar a CPU para produzir os elementos do enumerável várias vezes e a memória para armazenar os elementos da enumeração para acessá-los várias vezes ao usar métodos como ToList().