IEnumerable: O que Torna Algo "Percorrível"

IEnumerable: O que Torna Algo "Percorrível"

Por que arrays, listas, dicionários e conjuntos aceitam todos o mesmo foreach: todos cumprem o contrato IEnumerable, que declara apenas que os elementos podem ser percorridos. A aula mostra como escrever métodos que aceitam qualquer coleção e o que o foreach faz por baixo.
Linguagem C#

• • 11 min de leitura

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

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á.

Comentários

Mais em Linguagem C#

Concorrência: Fazendo Várias Coisas ao Mesmo Tempo
Concorrência: Fazendo Várias Coisas ao Mesmo Tempo

Como fazer um programa não travar enquanto espera: async e await. A aula…

Conectando a API ao Banco de Dados
Conectando a API ao Banco de Dados

A API ganha persistência real e, com ela, um conceito que estrutura aplicações…

Validação e Robustez em APIs
Validação e Robustez em APIs

O princípio inegociável de qualquer API: nunca confie na entrada. A aula cobre…