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.

Você vai aprender
  • 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 break para terminar antes.
  • Sequências infinitas, controladas com break ou Take.
  • A execução adiada: por que uma exceção só aparece no foreach.
  • O que acontece ao percorrer a mesma sequência duas vezes.
  • GetEnumerator com yield e os erros CS1622, CS1624 e CS1626.

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 foreach pede 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 de int que pode ser percorrida". É ele que permite o yield. (Também vale IEnumerator<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 return com 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:

  1. 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.
  2. As duas partes se revezam. O iterador entrega um item, o foreach trata esse item, o iterador continua, e assim por diante. Ninguém monta uma lista com os três números.
  3. O fim do método é o fim da sequência. O "[iterador] acabou" aparece quando o foreach pede 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 o i atual e pausa. Na volta seguinte, o i continua 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 return quiser, em qualquer lugar.
string.Join(" ", Pares(10))
O string.Join aceita qualquer IEnumerable: 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 return pausa e espera o próximo pedido.
(a, b) = (b, a + b);
Troca os dois de uma vez, com uma tupla: o novo a é o b, e o novo b é 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 do while, sem problema nenhum.
Fibonacci().Take(12)
O Take do LINQ ("pegar") pede só os 12 primeiros e para.
Nunca percorra uma sequência infinita até o fim

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.

A exceção aparece no 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 programa travou (ou a memória encheu)

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

EscritaO 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:

7;

2. Parar

Complete para a sequência terminar quando aparecer um número negativo:

if (n < 0) ;

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> (ou IEnumerator<T>) e usa yield return para 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 break encerra.
  • 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 break ou Take.
  • Não misture return x; com yield (CS1622), não use List<T> como retorno (CS1624) e não ponha yield num try com catch (CS1626).