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:
| Tipo | Acrescenta | Uso típico |
|---|---|---|
IEnumerable<T> | só foreach | Parâmetros que você só percorre; resultados de LINQ; sequências preguiçosas |
IReadOnlyCollection<T> | Count | Valores de retorno que quem chama não deve modificar |
ICollection<T> | Count, Add, Remove, Contains | Có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, RemoveAt | Código que edita por posição |
List<T> | A classe concreta que implementa tudo isso | Armazenamento |
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 umaList<T>. - Validar argumentos em um iterador. A verificação é adiada; divida em um método de fachada e um iterador.
- Chamar
Count()ouToList()em um iterador infinito. Nunca termina; useTakeantes. - Esperar que um parâmetro
IEnumerable<T>tenhaCountou[i]. Use oCount()do LINQ (que pode enumerar) ou aceiteIReadOnlyList<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.