Iterator Pattern and IEnumerable in C

This article explores the Iterator design pattern and how the .NET framework integrates it natively through IEnumerable and IEnumerator. You will learn the core concepts of the Iterator pattern, how the C# compiler handles collection traversal behind the scenes using the foreach loop, and how the yield return statement allows developers to implement custom iterators effortlessly without manually managing state machines.


What is the Iterator Pattern?

The Iterator pattern is a behavioral design pattern that allows you to sequentially traverse elements of a complex data structure—such as lists, trees, or hash tables—without exposing its underlying representation (e.g., arrays, nodes, or buckets).

The pattern decouples the traversal algorithm from the collection container itself. Traditionally, it consists of two main components:

  1. Iterator: An object responsible for tracking the current position and advancing to the next element.
  2. Aggregate (Iterable): An interface that provides a method to produce an instance of the Iterator.

By separating these responsibilities, multiple iterators can traverse the same collection independently and simultaneously without altering the collection's internal data.


The C# Abstraction: IEnumerable and IEnumerator

In C#, the Iterator pattern is standardized through two primary interfaces located in the System.Collections.Generic namespace:

  • IEnumerator<T> (The Iterator): Defines the mechanism for moving through the collection. It exposes:
    • T Current { get; }: Returns the element at the current position.
    • bool MoveNext(): Advances to the next item and returns true, or returns false if the end of the collection is reached.
    • void Reset(): Moves the pointer back before the first element (rarely used in modern C#).
    • void Dispose(): Inherited from IDisposable to release any unmanaged resources.
  • IEnumerable<T> (The Aggregate): Defines a single contract:
    • IEnumerator<T> GetEnumerator(): Returns an IEnumerator<T> used to iterate through the collection.

How C# Implements the Pattern Implicitly

C# simplifies the Iterator pattern to the point where developers rarely need to implement IEnumerator<T> manually. This implicit implementation happens at two major levels: consumption and creation.

1. Implicit Consumption: The foreach Statement

When you iterate over an IEnumerable using a foreach loop, the C# compiler translates that high-level syntax into the low-level mechanics of the Iterator pattern at compile time.

Consider this foreach loop:

List<int> numbers = new List<int> { 1, 2, 3 };

foreach (int number in numbers)
{
    Console.WriteLine(number);
}

The C# compiler lowers this code into the following equivalent logic:

List<int>.Enumerator enumerator = numbers.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        int number = enumerator.Current;
        Console.WriteLine(number);
    }
}
finally
{
    enumerator.Dispose();
}

The consumer never manages the pointer or the state directly; the compiler handles initialization, boundary checking, advancement, and resource disposal implicitly.

2. Implicit Creation: The yield return Keyword

Historically, creating a custom iterator required writing a dedicated class that implemented IEnumerator<T>, tracking index positions, handling edge cases, and storing state.

C# eliminates this boilerplate using the yield return keyword. When a method returns IEnumerable<T> or IEnumerator<T> and contains yield, the compiler automatically generates a hidden, private state machine class behind the scenes.

public static IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (int num in source)
    {
        if (num % 2 == 0)
        {
            yield return num;
        }
    }
}

When this method is called:

  1. Execution does not run the method body immediately. Instead, it returns a compiler-generated object that implements IEnumerable<int> and IEnumerator<int>.
  2. Execution pauses at each yield return until the consumer calls MoveNext().
  3. The compiler-generated state machine preserves local variables and the execution pointer between calls, providing lazy evaluation without requiring the developer to manually write an iterator class.