Ao longo do curso, você percorreu muitas coleções com o foreach: arrays, listas, dicionários, conjuntos. E deve ter notado algo notável — o foreach funciona identicamente em todas elas, estruturas internamente muito diferentes. Isso não é coincidência; é o resultado de um conceito unificador que está por trás de todas as coleções do C#, e que é a chave para entender não só o foreach, mas também a poderosa ferramenta de consulta de dados que veremos na próxima aula. Esse conceito chama-se IEnumerable, e ele responde a uma pergunta fundamental: o que torna algo "percorrível"? Nesta aula, entenderemos essa ideia, veremos o que o foreach realmente faz por baixo dos panos, e prepararemos o terreno para o LINQ. É uma aula que amarra e aprofunda tudo o que você já sabe sobre coleções.
A observação que motiva a aula
Comecemos pela observação concreta. Veja como o mesmo foreach percorre coleções completamente diferentes:
int[] array = { 1, 2, 3 };
List<string> lista = new() { "a", "b", "c" };
HashSet<int> conjunto = new() { 10, 20, 30 };
foreach (var item in array) { Console.WriteLine(item); } // funciona
foreach (var item in lista) { Console.WriteLine(item); } // funciona
foreach (var item in conjunto) { Console.WriteLine(item); } // funciona
Um array é uma fileira de posições contíguas na memória; uma List gerencia internamente um array que ela redimensiona; um HashSet usa uma estrutura bem diferente para garantir unicidade. São implementações distintas — e, no entanto, o foreach as trata todas igualmente. Como isso é possível? A resposta é que todas essas coleções, apesar de diferentes por dentro, compartilham uma capacidade comum: a de entregar seus elementos, um de cada vez, quando solicitadas. E essa capacidade comum tem um nome e uma forma no C#.
O contrato IEnumerable
Lembre-se das interfaces, que estudamos na Aula #30: uma interface é um contrato que declara uma capacidade, sem dizer como ela é implementada, permitindo que tipos diferentes a cumpram cada um à sua maneira. Pois bem: IEnumerable é justamente uma interface — um contrato —, e a capacidade que ela declara é a de "ser percorrível", isto é, saber entregar seus elementos um a um. Toda coleção do C# — array, List, Dictionary, HashSet — cumpre o contrato IEnumerable. É por isso que o foreach funciona com todas: ele não precisa conhecer os detalhes internos de cada uma; basta que todas cumpram o contrato "sei entregar meus elementos um a um", e o foreach sabe conversar com qualquer coisa que cumpra esse contrato.
Isto é a interface da Aula #30 mostrando seu valor na prática. Assim como um método que recebe um IReprodutivel funciona com qualquer objeto que saiba tocar e pausar, o foreach funciona com qualquer objeto que cumpra IEnumerable — que saiba ser percorrido. O contrato esconde as diferenças internas e expõe apenas a capacidade comum. Um array e um HashSet não têm nada em comum por dentro, mas ambos sabem ser percorridos, e é essa capacidade compartilhada, formalizada pelo contrato IEnumerable, que os torna intercambiáveis aos olhos do foreach.
Escrevendo métodos que aceitam qualquer coleção
Compreender o IEnumerable traz um benefício prático imediato e valioso: você pode escrever métodos que aceitam qualquer coleção percorrível, em vez de amarrar-se a um tipo específico. Se um método declara seu parâmetro como IEnumerable<int>, ele aceita um array de inteiros, uma List<int>, um HashSet<int> — qualquer coleção de inteiros que saiba ser percorrida:
// Este método aceita QUALQUER coleção de inteiros percorrível:
static int Somar(IEnumerable<int> numeros)
{
int soma = 0;
foreach (int n in numeros)
{
soma += n;
}
return soma;
}
// Funciona com tipos diferentes, sem alterar o método:
Console.WriteLine(Somar(new int[] { 1, 2, 3 })); // 6 — um array
Console.WriteLine(Somar(new List<int> { 10, 20 })); // 30 — uma List
Console.WriteLine(Somar(new HashSet<int> { 5, 5, 5 })); // 5 — um HashSet (duplicatas ignoradas)
O método Somar não sabe — nem precisa saber — se está recebendo um array, uma lista ou um conjunto. Ele só sabe que recebeu algo percorrível (um IEnumerable<int>), e o percorre com foreach. Isso o torna genérico e reutilizável: um único Somar serve para todas as coleções de inteiros, presentes e futuras. Aceitar IEnumerable em vez de um tipo concreto é uma marca de bom código em C#, pois torna seus métodos flexíveis e amplamente aplicáveis. É a interface trabalhando a seu favor: você programa contra a capacidade (ser percorrível), não contra uma implementação específica.
O que o foreach faz por baixo
Vale espiar, sem grande profundidade, o que o foreach realmente faz por baixo dos panos — pois desmistifica a "mágica" e prepara a próxima aula. Quando você escreve um foreach, o C# faz, nos bastidores, algo assim: pede à coleção que lhe entregue seus elementos, um de cada vez, repetindo dois passos — "há um próximo elemento?" e, se sim, "me dê o próximo". Ele continua enquanto houver elementos, e para quando a coleção sinaliza que acabaram. Você não escreve esses passos; o foreach os executa por você, e o contrato IEnumerable é justamente o que garante que a coleção sabe responder a essas duas perguntas — "há mais?" e "qual é o próximo?".
O detalhe importante a extrair é: os elementos são entregues um de cada vez, sob demanda. A coleção não precisa "despejar" todos os elementos de uma vez; ela os fornece conforme o foreach os pede. Essa ideia de entrega sob demanda — um elemento por vez, quando solicitado — é sutil agora, mas terá uma consequência elegante quando estudarmos o LINQ na próxima aula, onde ela permite processar dados de forma eficiente, calculando apenas o necessário. Por ora, guarde: percorrer é um diálogo de "há mais? me dê o próximo", e o IEnumerable é o contrato que torna esse diálogo possível com qualquer coleção.
Uma visão de conjunto
Recapitulando o que esta aula unificou: todas as coleções do C# — por mais diferentes que sejam por dentro — compartilham a capacidade de serem percorridas, formalizada pela interface IEnumerable. É essa capacidade comum que permite ao foreach tratá-las uniformemente, e que permite a você escrever métodos que aceitam qualquer coleção percorrível. O conceito da Aula #30 (interfaces como contratos de capacidade) e o conceito da Fase 4 (coleções) se encontram aqui: IEnumerable é a interface que unifica todas as coleções sob a capacidade de "ser percorrível". Esse entendimento não apenas amarra o que você já sabia, como abre a porta para a próxima ferramenta, que se ergue inteiramente sobre essa fundação.
A observação que motivou a aula ganhou explicação: arrays, listas, dicionários e conjuntos aceitam todos o mesmo foreach porque todos cumprem um mesmo contrato. IEnumerable é esse contrato, e ele declara apenas uma coisa — que os elementos podem ser percorridos um a um.
O proveito prático é imediato. Um método que recebe IEnumerable em vez de uma coleção concreta passa a aceitar qualquer uma delas, sem sobrecargas duplicadas e sem conversões. É a mesma lição das interfaces, agora na forma que mais aparece no código do dia a dia, e é também a base sobre a qual se apoia o recurso mais expressivo da linguagem para consultar dados.
Fontes e leituras recomendadas
- A interface IEnumerable\<T\> — a referência oficial do contrato que torna algo percorrível; a base desta aula.
- A instrução foreach — como o
foreachusa oIEnumerablepor baixo dos panos. - Interfaces como contratos (revisão) — o conceito de interface que fundamenta o
IEnumerable, revisto no contexto de coleções. - Escrevendo métodos genéricos com IEnumerable — por que aceitar
IEnumerabletorna métodos flexíveis. - Entrega sob demanda (conceito) — a ideia de fornecer elementos um a um, que o LINQ aproveitará.
Exercícios
Exercício 1
Escreva um método void ExibirTodos(IEnumerable<string> itens) que percorra e exiba cada item recebido. Chame-o passando um string[], uma List<string> e um HashSet<string>, e confirme que o mesmo método funciona com os três. Explique por que isso é possível.
Ver resposta
✓ Resposta: Exemplo:
static void ExibirTodos(IEnumerable<string> itens)
{
foreach (string item in itens)
Console.WriteLine(item);
}
ExibirTodos(new string[] { "a", "b" });
ExibirTodos(new List<string> { "c", "d" });
ExibirTodos(new HashSet<string> { "e", "f" });
O mesmo método funciona com os três porque todos — array, List e HashSet — cumprem o contrato IEnumerable<string>, ou seja, todos sabem ser percorridos. O método depende apenas dessa capacidade comum (declarada pelo parâmetro IEnumerable<string>), não da implementação concreta de cada coleção, então aceita qualquer uma que seja percorrível.
Exercício 2
Explique, com suas palavras, o que significa dizer que "todas as coleções do C# cumprem o contrato IEnumerable". Como esse fato explica que o foreach funcione identicamente com um array, uma List, um Dictionary e um HashSet?
Ver resposta
✓ Resposta: Dizer que todas as coleções do C# cumprem o contrato IEnumerable significa que todas elas, apesar de suas diferenças internas, se comprometem a oferecer a capacidade de "ser percorrível" — de entregar seus elementos, um a um, quando solicitadas. Esse fato explica que o foreach funcione identicamente com todas porque o foreach não precisa conhecer os detalhes internos de cada coleção; ele só precisa que a coleção cumpra o contrato IEnumerable, e então sabe conversar com ela para obter os elementos. Como array, List, Dictionary e HashSet cumprem todos esse mesmo contrato, o foreach os trata de maneira uniforme, ainda que por dentro sejam estruturas completamente distintas.
Exercício 3
Escreva um método int Contar(IEnumerable<int> numeros) que conte quantos elementos há na coleção recebida (percorrendo-a e incrementando um contador). Teste-o com uma List e com um HashSet, e explique por que declarar o parâmetro como IEnumerable foi uma boa escolha.
Ver resposta
✓ Resposta: Exemplo:
static int Contar(IEnumerable<int> numeros)
{
int total = 0;
foreach (int n in numeros) total++;
return total;
}
Console.WriteLine(Contar(new List<int> { 1, 2, 3 })); // 3
Console.WriteLine(Contar(new HashSet<int> { 5, 6, 5 })); // 2 (duplicata ignorada)
Declarar o parâmetro como IEnumerable<int> foi uma boa escolha porque torna o método aplicável a qualquer coleção de inteiros percorrível — uma List, um HashSet, um array —, sem precisar escrever versões separadas para cada tipo. O método depende só da capacidade de ser percorrido, então serve para todas as coleções que a tenham.
Exercício 4
Relacione o IEnumerable com o conceito de interface que você aprendeu na Aula #30. Em que sentido IEnumerable é uma interface, e qual é a "capacidade" que ela declara? Compare com a interface IReprodutivel (de Tocar/Pausar) daquela aula.
Ver resposta
✓ Resposta: IEnumerable é uma interface no mesmo sentido da Aula #30: é um contrato que declara uma capacidade sem impor uma implementação, permitindo que tipos diferentes o cumpram cada um à sua maneira. A capacidade que IEnumerable declara é a de "ser percorrível" — saber entregar os próprios elementos um a um. A comparação com IReprodutivel é direta: assim como IReprodutivel declarava a capacidade de "tocar e pausar" (e Musica e Video a cumpriam de formas diferentes), IEnumerable declara a capacidade de "ser percorrido" (e array, List, HashSet a cumprem de formas diferentes). Em ambos os casos, a interface une tipos distintos por uma capacidade comum, permitindo tratá-los uniformemente através do contrato.
Exercício 5
Sem código: a aula afirma que o foreach, por baixo, faz um "diálogo" com a coleção, repetindo duas perguntas. Quais são essas duas perguntas, e como o contrato IEnumerable se relaciona com elas? Por que a entrega "um de cada vez, sob demanda" será importante para o LINQ?
Ver resposta
✓ Resposta: As duas perguntas do "diálogo" são: "há um próximo elemento?" e, se houver, "me dê o próximo elemento". O foreach as repete até a coleção sinalizar que não há mais elementos. O contrato IEnumerable se relaciona com elas porque é justamente ele que garante que a coleção sabe responder a essas duas perguntas — cumprir IEnumerable significa saber informar se há mais elementos e fornecer o próximo quando pedido. A entrega "um de cada vez, sob demanda" será importante para o LINQ porque permite processar os dados de forma eficiente: como os elementos são fornecidos apenas quando solicitados, o LINQ poderá, por exemplo, parar de calcular assim que já tiver o que precisa, sem gerar ou percorrer elementos desnecessários — uma economia que a próxima aula explorará.