C# IEnumerable e foreach

Você já usou o foreach com arrays, listas, dicionários, conjuntos, filas e até com textos. Como um único comando sabe percorrer coisas tão diferentes? O segredo é uma interface chamada IEnumerable<T>: todas essas coleções a têm, e o foreach só precisa dela. Nesta aula você vai ver o que o foreach faz por dentro, escrever métodos que aceitam qualquer coleção e fazer uma classe sua funcionar no foreach.

Você vai aprender
  • O que é IEnumerable<T>: "algo que pode ser percorrido, um item por vez".
  • Escrever um método que aceita array, List, HashSet, Queue…
  • O que o foreach faz por dentro: GetEnumerator, MoveNext e Current.
  • Fazer uma classe sua funcionar no foreach.
  • As regras do foreach: a variável não pode ser trocada (CS1656) e a coleção não pode mudar no meio (InvalidOperationException).

Antes, é bom conhecer Interfaces e List. 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.


A ideia: "me dê o próximo"

Pense numa caixa de bombons, num baralho e numa playlist. São coisas diferentes, mas todas permitem a mesma conversa:

  você:  "tem mais algum?"     coleção:  "tem."
  você:  "qual é?"             coleção:  "o bombom de morango."
  você:  "tem mais algum?"     coleção:  "tem."
  você:  "qual é?"             coleção:  "o de coco."
  você:  "tem mais algum?"     coleção:  "acabou."

Para percorrer, você não precisa saber como a coleção guarda os itens por dentro (lado a lado, em gavetas de hash, em nós engatados…). Só precisa dessa conversa. No C#, a conversa tem nome: quem sabe fazê-la implementa a interface IEnumerable<T>, "enumerável de T", ou seja, "algo cujos itens do tipo T podem ser enumerados, um por um".

ColeçãoÉ um…O foreach entrega
int[]IEnumerable<int>cada número
List<string>, HashSet<string>, Queue<string>, Stack<string>IEnumerable<string>cada texto
Dictionary<string, int>IEnumerable<KeyValuePair<string, int>>cada par chave-valor
stringIEnumerable<char>cada letra

1. Um método que aceita qualquer coleção

Como todas as coleções são IEnumerable<T>, um método que recebe IEnumerable<int> aceita qualquer uma delas:

int[] array = [3, 1, 2];
List<int> lista = [10, 20];
HashSet<int> conjunto = [7, 8, 9];
var fila = new Queue<int>([100, 200]);

Mostrar("array", array);
Mostrar("lista", lista);
Mostrar("conjunto", conjunto);
Mostrar("fila", fila);

void Mostrar(string nome, IEnumerable<int> numeros)
{
    int soma = 0;
    foreach (int n in numeros)
        soma += n;
    Console.WriteLine($"{nome}: soma {soma}");
}
$ dotnet run todos.cs
array: soma 6
lista: soma 30
conjunto: soma 24
fila: soma 300
IEnumerable<int> numeros
O parâmetro diz: "me dê qualquer coisa que eu possa percorrer e que entregue int". O array, a lista, o conjunto e a fila servem, porque todos implementam essa interface.
foreach (int n in numeros)
O método só percorre. Ele não sabe (nem precisa saber) qual coleção recebeu.

Isso se chama programar para a interface: peça o mínimo de que você precisa. Se o método só percorre, peça IEnumerable<T>, e ele vai funcionar com coleções que você nem imaginou quando o escreveu.

IEnumerable só sabe percorrer

Por ser o mínimo, IEnumerable<T> não tem Add, Remove, Count (a propriedade) nem [ ]. Dentro do método, numeros.Add(5) dá error CS1061: ‘IEnumerable<int>’ não contém uma definição para "Add". Se o método precisa acrescentar itens, peça uma List<T> (ou a interface ICollection<T>). O Count() com parênteses que aparece em códigos por aí é do LINQ: ele percorre a coleção inteira contando.

Missão: o método Maior pede uma List<int>, por isso a chamada com o array e com o conjunto não compila (rode e veja o erro CS1503). Troque só o tipo do parâmetro para IEnumerable<int>, e as três chamadas vão funcionar.

int[] array = [4, 9, 2];
List<int> lista = [15, 3];
HashSet<int> conjunto = [40, 10];

Console.WriteLine($"array: maior {Maior(array)}");
Console.WriteLine($"lista: maior {Maior(lista)}");
Console.WriteLine($"conjunto: maior {Maior(conjunto)}");

int Maior(List<int> numeros)
{
    int maior = int.MinValue;
    foreach (int n in numeros)
        if (n > maior)
            maior = n;
    return maior;
}
int[] array = [4, 9, 2];
List<int> lista = [15, 3];
HashSet<int> conjunto = [40, 10];

Console.WriteLine($"array: maior {Maior(array)}");
Console.WriteLine($"lista: maior {Maior(lista)}");
Console.WriteLine($"conjunto: maior {Maior(conjunto)}");

int Maior(IEnumerable<int> numeros)
{
    int maior = int.MinValue;
    foreach (int n in numeros)
        if (n > maior)
            maior = n;
    return maior;
}

2. O que o foreach faz por dentro

A "conversa" da ideia lá do começo tem três peças, e você pode fazê-la à mão. Os dois laços abaixo fazem a mesma coisa:

List<string> frutas = ["maçã", "banana", "uva"];

foreach (string f in frutas)
{
    Console.WriteLine(f);
}

Console.WriteLine("--- o mesmo, à mão ---");
using (IEnumerator<string> marcador = frutas.GetEnumerator())
{
    while (marcador.MoveNext())
    {
        string f = marcador.Current;
        Console.WriteLine(f);
    }
}
$ dotnet run pordentro.cs
maçã
banana
uva
--- o mesmo, à mão ---
maçã
banana
uva
frutas.GetEnumerator()
"Pegar o enumerador." É o único método de IEnumerable<T>. Ele devolve um marcador novo, um IEnumerator<T> (repare: enumerator, "o que enumera"), parado antes do primeiro item, como um marcador de página antes da primeira página.
marcador.MoveNext()
"Mover para o próximo." Avança o marcador e responde: true se chegou num item, false se a coleção acabou. É o "tem mais algum?".
marcador.Current
"O atual." O item onde o marcador está. É o "qual é?".
using ( … )
No fim, o marcador é descartado (ele é IDisposable; veja using e IDisposable). O foreach faz isso sozinho, inclusive se der exceção no meio.
InterfaceQuem éO que tem
IEnumerable<T>A coleção ("pode ser percorrida")GetEnumerator()
IEnumerator<T>O marcador de um percursoMoveNext(), Current, Dispose()

Por que separar a coleção do marcador? Porque você pode ter dois percursos ao mesmo tempo na mesma coleção (dois foreach, um dentro do outro, como em Laços Aninhados): cada foreach pede o seu próprio marcador, e um não atrapalha o outro.


3. Uma classe sua no foreach

A Ana criou uma classe Estante que guarda livros numa lista privada. Ao tentar percorrer a estante, o compilador reclama:

var estante = new Estante();
foreach (string livro in estante)
{
    Console.WriteLine(livro);
}

class Estante
{
    private readonly List<string> livros = ["Dom Casmurro", "Vidas Secas"];
}
$ dotnet run semGet.cs
/home/ana/cs/semGet.cs(2,26): error CS1579: A instrução foreach não pode operar em variáveis do tipo 'Estante' porque 'Estante' não contém uma definição da extensão ou instância pública para 'GetEnumerator'

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

A mensagem já diz o que falta: um GetEnumerator público. O jeito certo de dar isso à classe é implementar IEnumerable<string>. Como a estante guarda os livros numa List, ela pode simplesmente repassar o marcador da lista:

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);
}
Console.WriteLine(string.Join(" | ", estante));
Console.WriteLine(estante.Count());

class Estante : IEnumerable<string>
{
    private readonly List<string> livros = [];

    public void Guardar(string livro) => livros.Add(livro);

    public IEnumerator<string> GetEnumerator() => livros.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
$ dotnet run estante.cs
Dom Casmurro
Vidas Secas
O Cortiço
Dom Casmurro | Vidas Secas | O Cortiço      « o string.Join aceita qualquer IEnumerable
3                                           « o Count() do LINQ também
using System.Collections;
Traz a versão antiga, sem <T>, de IEnumerable e IEnumerator. Ela não vem nos using automáticos, e a classe vai precisar dela (logo abaixo).
class Estante : IEnumerable<string>
"A estante pode ser percorrida, e entrega textos."
public IEnumerator<string> GetEnumerator() => livros.GetEnumerator();
O método que o foreach procura. Ele repassa o marcador da lista interna: percorrer a estante é percorrer a lista. O => é o corpo curto de um método (veja Métodos).
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
A interface IEnumerable<T> "herda" a antiga IEnumerable, de antes dos generics, e por isso a classe tem de ter também o GetEnumerator sem <T>. Escrito com o nome da interface na frente, ele é uma implementação explícita: fica escondido, só para quem usa a interface antiga. Ele apenas chama o de cima. Essa linha é sempre igual: copie e cole.

Repare no ganho: com IEnumerable<string>, a estante passou a funcionar também com o string.Join, com o Count() e com todo o LINQ, sem uma linha a mais. E os livros continuam privados: quem usa a estante pode percorrê-la, mas não pode fazer Add direto na lista (veja Encapsulamento).

E se os itens não estão numa lista?

Aqui foi fácil porque havia uma List para repassar. Quando os itens são calculados (os números de 1 a 100, as linhas de um arquivo…), o C# tem um atalho que escreve o marcador para você: o yield return, assunto da próxima aula, Iteradores.


4. As regras do foreach

A variável do laço não pode ser trocada

int[] notas = [7, 8, 9];
foreach (int n in notas)
{
    n = n + 1;
}
$ dotnet run iteracao.cs
/home/ana/cs/iteracao.cs(4,5): error CS1656: Não é possível atribuir a "n" porque ele é um "variável de iteração foreach"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O n é uma cópia do item atual (o Current), só para leitura. Mudá-la não mudaria o array, e o compilador proíbe para evitar a confusão. Para mudar os itens de um array ou de uma lista, use um for com índice: notas[i] = notas[i] + 1;.

A coleção não pode mudar no meio do percurso

List<string> nomes = ["Ana", "Bruno", "Carla"];
foreach (string nome in nomes)
{
    if (nome == "Bruno")
        nomes.Remove(nome);
}
$ dotnet run modificada.cs
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<Main>$(String[] args) in /home/ana/cs/modificada.cs:line 2
$ echo $?
134

O marcador da lista anota a "versão" da lista quando é criado. Cada Add ou Remove muda a versão. No MoveNext seguinte, o marcador percebe que a lista não é mais a mesma e lança a InvalidOperationException: "a coleção foi modificada; a operação de enumeração não pode continuar". Por isso o rastro aponta a linha 2, a do foreach, e não a do Remove. As outras coleções fazem o mesmo (veja Dictionary e HashSet).

Dois consertos comuns:

List<string> nomes = ["Ana", "Bruno", "Carla"];
foreach (string nome in nomes.ToList())
{
    if (nome == "Bruno")
        nomes.Remove(nome);
}
Console.WriteLine(string.Join(", ", nomes));

nomes.RemoveAll(n => n.StartsWith("C"));
Console.WriteLine(string.Join(", ", nomes));
$ dotnet run modificada2.cs
Ana, Carla
Ana
foreach (string nome in nomes.ToList())
O ToList() faz uma cópia da lista. O foreach percorre a cópia, e você pode mexer à vontade na original.
nomes.RemoveAll(n => n.StartsWith("C"))
A List tem um método pronto para "remover todos que…". O n => … é uma lambda com a condição.

Quando dá errado

error CS1579: A instrução foreach não pode operar em variáveis do tipo 'Estante' porque 'Estante' não contém uma definição … para 'GetEnumerator'

O que significa: você usou foreach num objeto que não sabe ser percorrido. Em inglês: foreach statement cannot operate on variables of type 'Estante' because 'Estante' does not contain a public instance or extension definition for 'GetEnumerator'.

Como consertar: se a classe é sua, implemente IEnumerable<T> (seção 3). Se não é, talvez exista uma propriedade com a coleção dentro dela (como Keys e Values do dicionário).

error CS0738: "Estante" não implementa membro de interface "IEnumerable.GetEnumerator()"…

O que significa: a classe diz : IEnumerable<string>, mas só tem o GetEnumerator com <T>. Falta o da versão antiga:

$ dotnet run faltaNaoGenerico.cs
/home/ana/cs/faltaNaoGenerico.cs(3,17): error CS0738: "Estante" não implementa membro de interface "IEnumerable.GetEnumerator()". "Estante.GetEnumerator()" não pode implementar "IEnumerable.GetEnumerator()" porqu não tem o tipo de retorno correspondente de "IEnumerator".

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

(O "porqu" sem "e" está assim mesmo na tradução do compilador.)

Como consertar: acrescente using System.Collections; no topo e a linha IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); na classe.

error CS1656: Não é possível atribuir a "n" porque ele é um "variável de iteração foreach"

O que significa: você tentou trocar o valor da variável do foreach.

Como consertar: para calcular algo, use outra variável (int maisUm = n + 1;). Para mudar os itens da coleção, use for com índice.

InvalidOperationException: Collection was modified; enumeration operation may not execute.

O que significa: a coleção ganhou ou perdeu itens enquanto um foreach a percorria.

Como consertar: percorra uma cópia (.ToList()), use RemoveAll, ou anote as mudanças numa lista e faça-as depois do laço (seção 4).

error CS1061: ‘IEnumerable<string>’ não contém uma definição para "Add"…

O que significa: o parâmetro é IEnumerable<T>, que só sabe percorrer.

Como consertar: se o método precisa acrescentar, peça List<T> ou ICollection<T>. Se só precisa ler, tire o Add e devolva uma coleção nova.


Referência rápida

EscritaO que é
IEnumerable<T>"Pode ser percorrido e entrega T". Tem só GetEnumerator()
IEnumerator<T>O marcador: MoveNext(), Current, Dispose()
void M(IEnumerable<int> xs)Método que aceita qualquer coleção de int
class C : IEnumerable<T>Classe sua no foreach; precisa dos dois GetEnumerator
public IEnumerator<T> GetEnumerator() => lista.GetEnumerator();Repassa o marcador de uma lista interna
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();A versão antiga, sem <T> (com using System.Collections;)
foreach (var x in colecao.ToList())Percorre uma cópia, para poder mudar a original

Exercícios

1. O tipo do parâmetro

Complete para o método aceitar array, lista ou conjunto de textos:

void Listar( nomes)

2. Tem mais algum?

Complete o laço feito à mão:

while (marcador.) Console.WriteLine(marcador.Current);

3. Qual é o único método que a interface IEnumerable<T> exige?

4. Dentro de um foreach sobre nomes (uma List), você chama nomes.Remove(nome). O que acontece?

5. Por que é bom um método pedir IEnumerable<T> em vez de List<T>, quando ele só percorre?

Missão final: a estante no foreach

Missão: rode e veja o erro CS1579. Faça a classe Estante implementar IEnumerable<string>, repassando o GetEnumerator da lista livros (não esqueça a versão sem <T> e o using System.Collections;), para o foreach mostrar os três livros.

Livro: Dom Casmurro
Livro: Vidas Secas
Livro: O Cortiço
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: {livro}");
}

class Estante
{
    private readonly List<string> livros = [];

    public void Guardar(string livro) => livros.Add(livro);
}
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: {livro}");
}

class Estante : IEnumerable<string>
{
    private readonly List<string> livros = [];

    public void Guardar(string livro) => livros.Add(livro);

    public IEnumerator<string> GetEnumerator() => livros.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Resumo

  • IEnumerable<T> quer dizer "pode ser percorrido, um T por vez". Arrays, todas as coleções e até string são IEnumerable.
  • Um método que só percorre deve pedir IEnumerable<T>: assim aceita qualquer coleção. Mas ele não tem Add nem [ ].
  • Por dentro, o foreach pede um marcador (GetEnumerator), avança com MoveNext, lê Current e descarta o marcador no fim.
  • Uma classe sua entra no foreach implementando IEnumerable<T>, com os dois GetEnumerator (sem eles: CS1579 ou CS0738).
  • A variável do foreach é só leitura (CS1656), e a coleção não pode ganhar nem perder itens no meio (InvalidOperationException): percorra uma cópia ou use RemoveAll.