As Interfaces de Iteração: IEnumerator e IEnumerable
No ecossistema .NET, a iteração sobre coleções de dados é governada por um protocolo padrão baseado em duas interfaces fundamentais. A primeira delas é a IEnumerator, que define os mecanismos de baixo nível para percorrer uma sequência de elementos de forma unidirecional.
A estrutura básica da interface IEnumerator é a seguinte:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
- MoveNext: Desloca o cursor para o próximo item da coleção. Retorna
falsese o final da sequência for atingido. - Current: Obtém o elemento na posição atual do cursor. É necessário chamar
MoveNextao menos uma vez antes de acessar esta propriedade. - Reset: Reposiciona o enumerador no estado inicial, antes do primeiro elemento. Na prática, este método é raramente utilizado, pois é mais eficiente instanciar um novo enumerador.
Geralmente, as coleções não implementam a lógica de enumeração diretamente em sua classe principal. Em vez disso, elas implementam a interface IEnumerable, que atua como uma fábrica de enumeradores:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
Ao delegar a lógica de iteração para um objeto separado (o enumerador), o C# permite que múltiplos consumidores percorram a mesma coleção simultaneamente, mantendo estados de cursor independentes.
Uso Manual vs. Açúcar Sintático com foreach
Embora seja possível interagir diretamente com o enumerador, essa abordagem é verbosa e propensa a erros. Veja um exemplo de iteração manual sobre uma sequência de caracteres:
string palavra = "Dev";
IEnumerator iterador = palavra.GetEnumerator();
while (iterador.MoveNext())
{
char letra = (char)iterador.Current;
Console.WriteLine($"Letra: {letra}");
}
O C# simplifica drasticamente este processo através da instrução foreach, que internamente executa o mesmo fluxo de controle, lidando inclusive com o descarte de recursos:
foreach (char letra in palavra)
{
Console.WriteLine($"Letra: {letra}");
}
Tipagem Forte com IEnumerable<T> e IEnumerator<T>
Com a introdução dos tipos genéricos, surgiram as versões IEnumerable<T> e IEnumerator<T>. Elas são preferíveis em quase todos os cenários modernos por oferecerem segurança de tipos em tempo de compilação e eliminarem o custo de boxing (conversão de tipos de valor para objeto).
public interface IEnumerator<T> : IEnumerator, IDisposable
{
T Current { get; }
}
public interface IEnumerable<T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
Um ponto crucial é que IEnumerator<T> herda de IDisposable. Isso garante que, ao final de uma iteração (especialmente dentro de um bloco foreach), qualquer recurso externo — como conexões de banco de dados ou manipuladores de arquivos — seja devidamente liberado.
Ao implementar coleções personalizadas, a prática recomandada é expor a interface genérica publicamente e implementar a versão não genérica de forma explícita. Isso mantém a API limpa e prioriza o desempenho, garantindo compatibildiade com códigos legados que ainda dependem da interface IEnumerable original.