A yield
palavra-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 foreach
loop. No entanto, um foreach
loop 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 yield
instruções intercaladas com outro código.
Para enumerar o bloco iterador, um foreach
loop é 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 foreach
açú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 Where
cláusula cria um novo IEnumerable<T>
que agrupa o IEnumerable<T>
retornado por, IteratorBlock
mas esse enumerável ainda não foi enumerado. Isso acontece quando você executa um foreach
loop:
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 foreach
circuito 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()
.