Menu

IEnumerable e yield return em C#: sequências preguiçosas explicadas

IEnumerable<T> é a interface por trás do foreach e do LINQ, e yield return é a forma mais fácil de implementá-la. Veja como IEnumerable se relaciona com ICollection e List, como um método iterador executa passo a passo, yield break, sequências infinitas e as armadilhas da avaliação preguiçosa.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Todo laço foreach e toda consulta LINQ funcionam por meio de uma pequena interface: IEnumerable<T>. Entendê-la explica por que o LINQ é preguiçoso, por que algumas sequências só podem ser lidas uma vez e como yield return permite escrever suas próprias sequências com um laço comum.

O que é IEnumerable

IEnumerable<T> tem um único método, GetEnumerator(). O enumerador que ele retorna tem MoveNext() (avança e informa se há um elemento) e Current (o elemento). Um foreach é uma forma abreviada de conduzir esse enumerador:

Saída:

red green blue 
red green blue 

O enumerador começa antes do primeiro elemento, então o primeiro MoveNext() se move para ele. Quando MoveNext() retorna false, a sequência acabou. O bloco using descarta o enumerador, o que importa para iteradores que seguram recursos como arquivos abertos.

IEnumerable vs ICollection vs List

As interfaces de coleção formam uma escada. Cada degrau promete mais:

TipoAcrescentaUso típico
IEnumerable<T>só foreachParâmetros que você só percorre; resultados de LINQ; sequências preguiçosas
IReadOnlyCollection<T>CountValores de retorno que quem chama não deve modificar
ICollection<T>Count, Add, Remove, ContainsCódigo que adiciona a uma coleção de qualquer tipo
IReadOnlyList<T>Acesso por índice [i]Resultados somente leitura com posições
IList<T>Acesso por índice, Insert, RemoveAtCódigo que edita por posição
List<T>A classe concreta que implementa tudo issoArmazenamento

Um método que só percorre a entrada deve aceitar IEnumerable<T>, para que quem chama possa passar um array, uma lista, um conjunto ou uma consulta LINQ sem converter. Um método que retorna dados normalmente deve retornar algo que diga "isto já está na memória", como List<T> ou IReadOnlyList<T>, porque um valor de retorno IEnumerable<T> pode ser uma consulta que faz trabalho toda vez que é lida.

Saída:

30
15
4.0
30

Escrevendo um iterador com yield return

Para produzir sua própria sequência, escreva um método que retorna IEnumerable<T> e use yield return para cada elemento. O compilador transforma o método em uma classe que implementa o enumerador para você.

Saída:

0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri 

Não existe lista nenhuma em EvenNumbers. Cada yield return entrega um valor a quem chama e pausa o método, mantendo as variáveis locais (i aqui) vivas até que quem chama peça o próximo valor.

Preguiça: observe a ordem de execução

Como o método pausa, o corpo dele e o laço de quem chama se revezam. Imprimir dos dois lados mostra a intercalação:

Saída:

Called Numbers(), nothing printed yet
  start
  yielding 1
Loop got 1
  resumed after 1
  yielding 2
Loop got 2
  resumed after 2
  yielding 3
Loop got 3
  resumed after 3
  end

Chamar Numbers() não imprimiu "start": só criou o enumerável. O corpo começou no primeiro MoveNext() dentro do foreach, executou até o primeiro yield return e parou. Cada passo seguinte continuou na linha depois do yield. O Where e o Select do LINQ se comportam do mesmo jeito (o runtime os implementa com classes de enumerador escritas à mão por velocidade, mas o efeito é idêntico), e é por isso que as consultas LINQ são adiadas.

yield break

yield break encerra a sequência antes, a versão do return para iteradores:

Saída:

Subject: Invoice
From: shop@example.com

Você não pode misturar return value; e yield return em um mesmo método. Quando um método contém yield, ele é um iterador, e yield break é a única forma de sair dele antes.

Sequências infinitas

Como só os elementos pedidos são calculados, um iterador pode descrever uma sequência sem fim. Quem consome decide quanto pegar:

Saída:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon

while (true) é seguro aqui porque Take e First param de pedir depois de ter o que precisam. Chamar ToList() ou Count() em uma sequência infinita, por outro lado, nunca retorna, e enumerar um OrderBy sobre ela também não, porque ordenar exige todos os elementos antes.

Armadilhas das sequências preguiçosas

As verificações de argumentos executam tarde. O corpo inteiro de um iterador é adiado, inclusive a validação no topo. A solução é dividir o método: um método normal verifica os argumentos e retorna o resultado de um iterador privado.

Saída:

LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call

Cada enumeração executa o método de novo. Um iterador não guarda nada em cache. Percorrer Numbers() duas vezes imprime "start" duas vezes, relê o arquivo duas vezes ou chama o serviço web duas vezes. As IDEs sinalizam isso como "possible multiple enumeration of IEnumerable". Chame ToList() uma vez quando você precisar dos elementos mais de uma vez.

O finally executa quando quem consome para. Se quem chama sai do foreach antes do fim, o enumerador é descartado e qualquer bloco finally do iterador executa, e é assim que um iterador que lê um arquivo o fecha:

Saída:

open sensor
10
20
30
close sensor
done

Algumas regras que o compilador impõe aos iteradores: yield return não pode aparecer dentro de um bloco try que tenha catch (um try/finally não tem problema), dentro de uma lambda ou método anônimo, nem em um método com parâmetros ref ou out.

Streams assíncronos

Para sequências cujos elementos chegam de forma assíncrona (páginas de uma API, linhas de um banco de dados), o C# 8 em diante (com .NET Core 3.0 ou posterior) combina iteradores com await por meio de IAsyncEnumerable<T>:

static async IAsyncEnumerable<string> FetchPages()
{
    for (int page = 1; page <= 3; page++)
    {
        await Task.Delay(100);           // simulate a network call
        yield return $"page {page}";
    }
}

await foreach (string p in FetchPages())
{
    Console.WriteLine(p);
}

Quem consome usa await foreach, e cada elemento é aguardado à medida que é produzido.

Erros comuns

  • Retornar IEnumerable<T> de uma consulta e lê-lo muitas vezes. Cada leitura a executa de novo; retorne uma List<T>.
  • Validar argumentos em um iterador. A verificação é adiada; divida em um método de fachada e um iterador.
  • Chamar Count() ou ToList() em um iterador infinito. Nunca termina; use Take antes.
  • Esperar que um parâmetro IEnumerable<T> tenha Count ou [i]. Use o Count() do LINQ (que pode enumerar) ou aceite IReadOnlyList<T>.

Perguntas frequentes

O que é IEnumerable em C#?

IEnumerable<T> é a interface de tudo o que pode ser percorrido com foreach. Ela tem um método, GetEnumerator(), que retorna um IEnumerator<T> com MoveNext() e Current. Arrays, listas, dicionários, conjuntos, consultas LINQ e métodos iteradores implementam essa interface.

Qual a diferença entre IEnumerable e List em C#?

IEnumerable<T> só promete que você pode iterar; não tem propriedade Count, nem indexador, nem Add, e pode ser calculado de forma preguiçosa a cada vez que você percorre. List<T> é uma coleção concreta guardada na memória com tudo isso. Aceite IEnumerable<T> em parâmetros quando você só percorre, e retorne uma List<T> ou IReadOnlyList<T> quando quem chama precisa de índice ou contagem.

O que o yield return faz em C#?

yield return value; dentro de um método que retorna IEnumerable<T> entrega um elemento a quem chama e pausa o método naquele ponto. Na próxima vez que quem chama pede um elemento, o método continua na linha seguinte. O compilador reescreve o método como uma máquina de estados, então você escreve um laço e ganha uma sequência preguiçosa.

O que é yield break em C#?

yield break; encerra a sequência: o foreach de quem chama termina normalmente depois dos elementos já produzidos. É o equivalente do return em um iterador e é usado para parar antes, por exemplo quando um limite é atingido ou a entrada está vazia.

Por que meu método iterador não lança exceção quando eu o chamo?

O corpo de um método iterador só executa quando a sequência é enumerada, então as verificações de argumentos no topo executam no primeiro MoveNext, não na chamada. Para validar imediatamente, coloque as verificações em um método público normal que depois chama um método iterador privado com as instruções yield.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR