C# Iteradores (yield return)
Na aula IEnumerable e foreach, a estante da Ana entrou no foreach porque tinha uma lista para repassar. Mas e quando os itens não estão guardados em lugar nenhum, e sim são calculados: os números pares até 100, as senhas de uma fila, a sequência de Fibonacci? Para isso o C# tem o yield return: você escreve um método comum, e ele vira uma sequência que entrega um item por vez, só quando alguém pede.
- O que é um iterador e como escrever um com
yield return. - Como o iterador pausa a cada item e continua de onde parou.
- Iteradores com laços, e o
yield breakpara terminar antes. - Sequências infinitas, controladas com
breakouTake. - A execução adiada: por que uma exceção só aparece no
foreach. - O que acontece ao percorrer a mesma sequência duas vezes.
GetEnumeratorcomyielde os errosCS1622,CS1624eCS1626.
Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Você pode repetir cada um no editor do guia.
A ideia: a máquina de senhas
Na padaria, a máquina de senhas não imprime todas as senhas do dia de manhã. Ela entrega uma quando alguém aperta o botão, e fica parada esperando o próximo aperto, lembrando em que número estava.
Um método comum é o contrário: ele roda do começo ao fim e devolve tudo de uma vez (com um return). Um iterador é um método que funciona como a máquina de senhas:
- a cada
yield return, ele entrega um item e pausa; - quando o
foreachpede o próximo, ele continua exatamente de onde parou, com as variáveis do jeito que estavam; - quando chega ao fim do método, a sequência acabou.
Yield, em inglês, é "entregar", "ceder a vez". yield return 5; se lê "entregue o 5 e ceda a vez".
1. O primeiro iterador
foreach (int n in UmDoisTres())
{
Console.WriteLine(n);
}
IEnumerable<int> UmDoisTres()
{
yield return 1;
yield return 2;
yield return 3;
}
$ dotnet run primeiro.cs 1 2 3
- IEnumerable<int> UmDoisTres()
- O tipo de retorno é
IEnumerable<int>: "uma sequência deintque pode ser percorrida". É ele que permite oyield. (Também valeIEnumerator<T>, como você verá na seção 5.) - yield return 1;
- Entrega o 1 para quem está percorrendo e pausa o método.
- sem return no fim
- O método não monta lista nenhuma nem tem
returncom valor. O compilador transforma o método inteiro numa "máquina" que sabe pausar e continuar; o fim do método é o fim da sequência.
Vendo a pausa acontecer
Para ver a ordem real das coisas, este programa mostra uma mensagem em cada passo, dentro e fora do iterador:
Console.WriteLine("Chamando o método...");
IEnumerable<int> numeros = UmDoisTres();
Console.WriteLine("Começando o foreach...");
foreach (int n in numeros)
{
Console.WriteLine($" o foreach recebeu {n}");
}
Console.WriteLine("Fim.");
IEnumerable<int> UmDoisTres()
{
Console.WriteLine(" [iterador] começou");
yield return 1;
Console.WriteLine(" [iterador] voltou depois do 1");
yield return 2;
Console.WriteLine(" [iterador] voltou depois do 2");
yield return 3;
Console.WriteLine(" [iterador] acabou");
}
$ dotnet run pausa.cs Chamando o método... Começando o foreach... « chamar o método não rodou nada dele! [iterador] começou « o 1º item foi pedido: agora ele começa o foreach recebeu 1 [iterador] voltou depois do 1 « continuou de onde tinha parado o foreach recebeu 2 [iterador] voltou depois do 2 o foreach recebeu 3 [iterador] acabou « o foreach pediu mais um: não tinha, fim Fim.
Repare em três coisas:
- Chamar o método não executa o método. A linha
UmDoisTres()só criou a sequência; o "[iterador] começou" apareceu depois do "Começando o foreach...". O código só roda quando alguém pede o primeiro item. - As duas partes se revezam. O iterador entrega um item, o
foreachtrata esse item, o iterador continua, e assim por diante. Ninguém monta uma lista com os três números. - O fim do método é o fim da sequência. O "[iterador] acabou" aparece quando o
foreachpede um quarto item que não existe.
Por dentro, é a conversa da aula anterior: cada MoveNext() do foreach faz o iterador rodar até o próximo yield return; o valor entregue vira o Current.
2. Iteradores com laços
O yield return fica ainda mais útil dentro de um laço: com três linhas, você descreve uma sequência inteira.
Console.WriteLine(string.Join(" ", Pares(10)));
Console.WriteLine(string.Join(" ", Regressiva(5)));
IEnumerable<int> Pares(int limite)
{
for (int i = 0; i <= limite; i += 2)
yield return i;
}
IEnumerable<string> Regressiva(int de)
{
for (int i = de; i >= 1; i--)
yield return i.ToString();
yield return "Decolar!";
}
$ dotnet run lacos.cs 0 2 4 6 8 10 5 4 3 2 1 Decolar!
- for (int i = 0; i <= limite; i += 2) yield return i;
- A cada volta do
for, entrega oiatual e pausa. Na volta seguinte, oicontinua valendo o que valia: o iterador lembra de tudo. - yield return "Decolar!";
- Depois do laço, mais um item. Um iterador pode ter quantos
yield returnquiser, em qualquer lugar. - string.Join(" ", Pares(10))
- O
string.Joinaceita qualquerIEnumerable: ele percorre a sequência e junta os itens.
Missão: complete o iterador Quadrados para entregar os quadrados de 1 até n (1×1, 2×2, …) com yield return. A saída de Quadrados(5) deve ser 1 4 9 16 25.
Console.WriteLine(string.Join(" ", Quadrados(5)));
IEnumerable<int> Quadrados(int n)
{
// para cada i de 1 até n, entregue i * i
yield break;
}
Console.WriteLine(string.Join(" ", Quadrados(5)));
IEnumerable<int> Quadrados(int n)
{
for (int i = 1; i <= n; i++)
yield return i * i;
}
yield break: terminar antes
O código inicial da missão tinha um yield break, que encerra a sequência na hora, como um return encerra um método comum ("pare de entregar"). Ele é útil para parar no meio de um laço:
foreach (string linha in LinhasAteFim(["Ana", "Bruno", "FIM", "Carla"]))
{
Console.WriteLine(linha);
}
IEnumerable<string> LinhasAteFim(string[] linhas)
{
foreach (string linha in linhas)
{
if (linha == "FIM")
yield break;
yield return linha;
}
}
$ dotnet run break.cs Ana Bruno « ao chegar no "FIM", a sequência acabou: a Carla nem foi vista
3. Sequências infinitas
Como o iterador só calcula quando alguém pede, ele pode descrever uma sequência sem fim. Quem percorre é que decide quando parar. A sequência de Fibonacci (cada número é a soma dos dois anteriores) é um exemplo clássico:
foreach (long f in Fibonacci())
{
if (f > 100)
break;
Console.Write($"{f} ");
}
Console.WriteLine();
Console.WriteLine(string.Join(", ", Fibonacci().Take(12)));
IEnumerable<long> Fibonacci()
{
long a = 0, b = 1;
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}
$ dotnet run infinita.cs 0 1 1 2 3 5 8 13 21 34 55 89 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
- while (true)
- Um laço que nunca termina sozinho. Num método comum, isso travaria o programa. Aqui não: a cada volta, o
yield returnpausa e espera o próximo pedido. - (a, b) = (b, a + b);
- Troca os dois de uma vez, com uma tupla: o novo
aé ob, e o novobé a soma. - if (f > 100) break;
- Quem decide parar é o
foreach(veja break e continue). Ao sair do laço, o iterador é descartado no meio dowhile, sem problema nenhum. - Fibonacci().Take(12)
- O
Takedo LINQ ("pegar") pede só os 12 primeiros e para.
Fibonacci().ToList(), Fibonacci().Count() ou um foreach sem break tentam chegar ao fim de algo que não tem fim: o programa fica preso (e, no caso do ToList, enche a memória). Sempre limite com break, Take ou uma condição.
4. A execução adiada e suas surpresas
"Chamar o método não executa o método" tem consequências que pegam muita gente.
A exceção que aparece no lugar "errado"
IEnumerable<int> numeros = Contar(-1);
Console.WriteLine("Chamei Contar(-1) e nada aconteceu.");
foreach (int n in numeros)
{
Console.WriteLine(n);
}
IEnumerable<int> Contar(int ate)
{
if (ate < 0)
throw new ArgumentOutOfRangeException(nameof(ate), "Não dá para contar até um número negativo.");
for (int i = 1; i <= ate; i++)
yield return i;
}
$ dotnet run atrasado.cs Chamei Contar(-1) e nada aconteceu. Unhandled exception. System.ArgumentOutOfRangeException: Não dá para contar até um número negativo. (Parameter 'ate') at Program.<<Main>$>g__Contar|0_0(Int32 ate)+MoveNext() in /home/ana/cs/atrasado.cs:line 12 at Program.<Main>$(String[] args) in /home/ana/cs/atrasado.cs:line 4 $ echo $? 134
A verificação do ate está na primeira linha do método, mas a exceção (veja throw) só estourou quando o foreach (linha 4) pediu o primeiro item. O rastro conta a história: quem lançou foi o MoveNext() do iterador (o nome estranho <<Main>$>g__Contar|0_0 é como o compilador batizou a "máquina" gerada a partir de Contar), chamado pela linha 4. Num programa grande, a chamada e o foreach podem estar em arquivos diferentes, e o erro parece vir do lugar errado.
Quando for importante recusar o argumento na hora da chamada, separe em dois métodos: um comum, que confere e depois chama o iterador:
IEnumerable<int> Contar(int ate)
{
if (ate < 0)
throw new ArgumentOutOfRangeException(nameof(ate), "Não dá para contar até um número negativo.");
return ContarDe1(ate);
}
IEnumerable<int> ContarDe1(int ate)
{
for (int i = 1; i <= ate; i++)
yield return i;
}
Como Contar não tem yield, ele é um método comum e roda inteiro na chamada; quem pausa é só o ContarDe1. Com a mesma primeira linha, IEnumerable<int> numeros = Contar(-1);, agora a exceção sai na própria chamada:
$ dotnet run separado.cs Unhandled exception. System.ArgumentOutOfRangeException: Não dá para contar até um número negativo. (Parameter 'ate') at Program.<<Main>$>g__Contar|0_0(Int32 ate) in /home/ana/cs/separado.cs:line 7 at Program.<Main>$(String[] args) in /home/ana/cs/separado.cs:line 1 « a linha da chamada
Percorrer duas vezes roda tudo duas vezes
IEnumerable<int> dados = Sortear();
Console.WriteLine($"Soma: {dados.Sum()}");
Console.WriteLine($"Maior: {dados.Max()}");
var lista = dados.ToList();
Console.WriteLine($"Lista com {lista.Count}");
Console.WriteLine($"Soma: {lista.Sum()}");
IEnumerable<int> Sortear()
{
Console.WriteLine(" (gerando os números)");
yield return 4;
yield return 9;
yield return 2;
}
$ dotnet run duasvezes.cs (gerando os números) « o Sum percorreu: o iterador rodou Soma: 15 (gerando os números) « o Max percorreu de novo: rodou de novo Maior: 9 (gerando os números) « o ToList percorreu uma última vez Lista com 3 Soma: 15 « a lista já está pronta: nada roda
Uma sequência de iterador não guarda nada: cada percurso (cada foreach, Sum, Max…) roda o método de novo, desde o começo. Se o iterador faz algo demorado (ler um arquivo, por exemplo) ou que muda a cada vez, isso pesa ou confunde. Quando você vai usar os itens mais de uma vez, guarde-os primeiro com .ToList().
5. GetEnumerator com yield
Na aula anterior, a estante repassava o GetEnumerator da lista. Com yield, você escreve o seu próprio percurso, por exemplo numerando os livros:
using System.Collections;
var estante = new Estante();
estante.Guardar("Dom Casmurro");
estante.Guardar("Vidas Secas");
estante.Guardar("O Cortiço");
foreach (string livro in estante)
Console.WriteLine(livro);
class Estante : IEnumerable<string>
{
private readonly List<string> livros = [];
public void Guardar(string livro) => livros.Add(livro);
public IEnumerator<string> GetEnumerator()
{
for (int i = 0; i < livros.Count; i++)
yield return $"{i + 1}. {livros[i]}";
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
$ dotnet run estante.cs 1. Dom Casmurro 2. Vidas Secas 3. O Cortiço
O GetEnumerator devolve IEnumerator<string>, o "marcador", e o yield return funciona com esse tipo também: o compilador escreve o MoveNext e o Current para você.
Quando dá errado
error CS1622: Não é possível retornar um valor de um iterador. Use a instrução yield return para retornar um valor ou yield break para finalizar a iteração.
O que significa: o método tem yield return e também um return x; comum. Num iterador, não dá para misturar:
$ cat erroReturn.cs
IEnumerable<int> Numeros()
{
yield return 1;
return 2;
}
Console.WriteLine(Numeros().First());
$ dotnet run erroReturn.cs
/home/ana/cs/erroReturn.cs(4,5): error CS1622: Não é possível retornar um valor de um iterador. Use a instrução yield return para retornar um valor ou yield break para finalizar a iteração.
Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Como consertar: para entregar mais um item, yield return 2;. Para encerrar, yield break;.
error CS1624: O corpo de "Numeros()" não pode ser um bloco de iteradores porque "List<int>" não é um tipo de interface de iterador
O que significa: o método com yield declara que devolve List<int>. O yield não monta uma lista: ele cria uma sequência.
Como consertar: o tipo de retorno tem de ser IEnumerable<T> ou IEnumerator<T>. Se quem chama precisa de uma lista, ele faz Numeros().ToList().
error CS1626: Não é possível usar a instrução yield no corpo de um bloco try com uma cláusula catch
O que significa: há um yield return dentro de um try que tem catch. O C# não permite (com try/finally, sem catch, pode).
Como consertar: faça a parte que pode falhar fora do try com yield: calcule o valor dentro de um try/catch, guarde numa variável e dê o yield return depois.
foreach, e não na chamada
O que significa: é a execução adiada (seção 4): o código do iterador, inclusive as verificações, só roda quando o primeiro item é pedido.
Como consertar: separe a verificação num método comum que chama o iterador.
O que significa: uma sequência infinita foi percorrida até o "fim" com ToList, Count ou um foreach sem break.
Como consertar: aperte Ctrl+C no terminal para parar o programa e limite a sequência com Take(n) ou break.
Referência rápida
| Escrita | O que faz |
|---|---|
IEnumerable<T> M() { … yield return x; … } | Iterador: entrega itens um por vez |
yield return x; | Entrega x e pausa até o próximo pedido |
yield break; | Encerra a sequência |
public IEnumerator<T> GetEnumerator() { … yield return … } | Percurso próprio de uma classe IEnumerable<T> |
seq.Take(n) | Só os n primeiros (seguro com sequências infinitas) |
seq.ToList() | Roda o iterador uma vez e guarda tudo numa lista |
Exercícios
1. Entregar um item
Complete para o iterador entregar o número 7:
2. Parar
Complete para a sequência terminar quando aparecer um número negativo:
3. Um iterador começa com Console.WriteLine("oi");. Quando o "oi" aparece?
4. Qual tipo de retorno não pode ter um método com yield return?
5. var s = Sortear(); e depois s.Sum() e s.Max(). Quantas vezes o código de Sortear roda?
Missão final: a máquina de senhas
Missão: escreva o iterador Senhas, que entrega senhas sem fim: A001, A002, A003… (dica: um while (true) com um contador e $"A{numero:D3}", que escreve o número com 3 dígitos). O foreach já está pronto e para depois de 3 senhas.
Senha A001 Senha A002 Senha A003
int chamadas = 0;
foreach (string senha in Senhas())
{
Console.WriteLine($"Senha {senha}");
chamadas++;
if (chamadas == 3)
break;
}
IEnumerable<string> Senhas()
{
// entregue "A001", "A002", "A003"... sem parar
yield break;
}
int chamadas = 0;
foreach (string senha in Senhas())
{
Console.WriteLine($"Senha {senha}");
chamadas++;
if (chamadas == 3)
break;
}
IEnumerable<string> Senhas()
{
int numero = 1;
while (true)
{
yield return $"A{numero:D3}";
numero++;
}
}
Resumo
- Um iterador é um método que devolve
IEnumerable<T>(ouIEnumerator<T>) e usayield returnpara entregar um item por vez. - A cada
yield return, o método pausa; no próximo pedido, continua de onde parou, com as variáveis intactas.yield breakencerra. - Chamar o iterador não roda nada: o código só executa quando alguém percorre (execução adiada). Por isso as exceções aparecem no
foreach. - Cada percurso roda o iterador de novo desde o começo; para usar várias vezes, guarde com
.ToList(). - Sequências infinitas são possíveis: limite com
breakouTake. - Não misture
return x;comyield(CS1622), não useList<T>como retorno (CS1624) e não ponhayieldnumtrycomcatch(CS1626).