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.
- 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
foreachfaz por dentro:GetEnumerator,MoveNexteCurrent. - 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 |
string | IEnumerable<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, umIEnumerator<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:
truese chegou num item,falsese 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). Oforeachfaz isso sozinho, inclusive se der exceção no meio.
| Interface | Quem é | O que tem |
|---|---|---|
IEnumerable<T> | A coleção ("pode ser percorrida") | GetEnumerator() |
IEnumerator<T> | O marcador de um percurso | MoveNext(), 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>, deIEnumerableeIEnumerator. Ela não vem nosusingautomá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
foreachprocura. 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 antigaIEnumerable, de antes dos generics, e por isso a classe tem de ter também oGetEnumeratorsem<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).
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. Oforeachpercorre a cópia, e você pode mexer à vontade na original. - nomes.RemoveAll(n => n.StartsWith("C"))
- A
Listtem um método pronto para "remover todos que…". On => …é 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
| Escrita | O 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:
2. Tem mais algum?
Complete o laço feito à mão:
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, umTpor vez". Arrays, todas as coleções e atéstringsãoIEnumerable.- Um método que só percorre deve pedir
IEnumerable<T>: assim aceita qualquer coleção. Mas ele não temAddnem[ ]. - Por dentro, o
foreachpede um marcador (GetEnumerator), avança comMoveNext, lêCurrente descarta o marcador no fim. - Uma classe sua entra no
foreachimplementandoIEnumerable<T>, com os doisGetEnumerator(sem eles:CS1579ouCS0738). - 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 useRemoveAll.