BrandGhost
Iterator Design Pattern in C#: Complete Guide with Examples

Iterator Design Pattern in C#: Complete Guide with Examples

Iterator Design Pattern in C#: Complete Guide with Examples

When you need to traverse a collection without caring about how its elements are stored, the iterator design pattern in C# is the behavioral pattern that makes it possible. It provides a uniform way to access elements sequentially -- regardless of whether the underlying structure is an array, a linked list, a tree, or something entirely custom. C# has first-class support for this pattern baked directly into the language through IEnumerable<T>, IEnumerator<T>, and the foreach keyword, making it one of the most frequently used design patterns in the .NET ecosystem.

In this complete guide, we'll cover the iterator design pattern from the ground up -- the core components and how they map to C# interfaces, a full custom implementation, how yield return simplifies iterator creation, custom iteration patterns like filtering and tree traversal, how LINQ leverages iterators under the hood, and practical guidance on when to apply the pattern. By the end, you'll have working code examples and a clear understanding of how iterators work in your .NET applications.

Understanding the Iterator Design Pattern

The iterator design pattern is a behavioral pattern from the Gang of Four (GoF) catalog that separates the traversal logic of a collection from the collection itself. The pattern defines two core roles: the iterator, which knows how to step through elements one at a time, and the aggregate (or collection), which knows how to create an iterator for itself.

In C#, these roles map directly to framework interfaces. The IEnumerable<T> interface represents the aggregate -- it exposes a single method, GetEnumerator(), that returns an iterator. The IEnumerator<T> interface represents the iterator itself, providing MoveNext() to advance to the next element, Current to access the element at the current position, and Reset() to return to the beginning.

The foreach loop is syntactic sugar that depends entirely on this pattern. When you write foreach (var item in collection), the compiler generates code that calls GetEnumerator() on the collection, then repeatedly calls MoveNext() and reads Current until MoveNext() returns false. The compiler also wraps the enumerator in a try/finally block to call Dispose() when iteration ends, ensuring resources are cleaned up properly.

This separation has a powerful consequence: any code that consumes an IEnumerable<T> doesn't need to know whether it's iterating over an array, a database cursor, a file stream, or a procedurally generated sequence. The iterator design pattern in C# decouples "what you're iterating" from "how you're iterating" -- and that decoupling shows up everywhere in the .NET framework.

Basic C# Implementation

Let's build a custom collection from scratch to see how the iterator design pattern in C# works at the implementation level. We'll create a FixedSizeBuffer<T> -- a ring buffer that wraps around when it reaches capacity -- and implement IEnumerable<T> so it works seamlessly with foreach.

Building the Custom Collection

The collection class implements IEnumerable<T> and provides its own enumerator:

using System;
using System.Collections;
using System.Collections.Generic;

public sealed class FixedSizeBuffer<T> : IEnumerable<T>
{
    private readonly T[] _items;
    private int _head;
    private int _count;

    public FixedSizeBuffer(int capacity)
    {
        if (capacity <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(capacity),
                "Capacity must be greater than zero.");
        }

        _items = new T[capacity];
    }

    public int Count => _count;

    public void Add(T item)
    {
        _items[_head] = item;
        _head = (_head + 1) % _items.Length;

        if (_count < _items.Length)
        {
            _count++;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new BufferEnumerator(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private sealed class BufferEnumerator : IEnumerator<T>
    {
        private readonly FixedSizeBuffer<T> _buffer;
        private int _index;
        private bool _started;

        public BufferEnumerator(FixedSizeBuffer<T> buffer)
        {
            _buffer = buffer;
            _index = -1;
            _started = false;
        }

        public T Current
        {
            get
            {
                if (!_started || _index >= _buffer._count)
                {
                    throw new InvalidOperationException(
                        "Enumerator is not positioned " +
                        "on a valid element.");
                }

                int actualIndex =
                    (_buffer._head - _buffer._count + _index
                     + _buffer._items.Length)
                    % _buffer._items.Length;

                return _buffer._items[actualIndex];
            }
        }

        object? IEnumerator.Current => Current;

        public bool MoveNext()
        {
            _started = true;
            _index++;
            return _index < _buffer._count;
        }

        public void Reset()
        {
            _index = -1;
            _started = false;
        }

        public void Dispose()
        {
            // No unmanaged resources to release
        }
    }
}

There's a lot happening here, so let's break down the key parts of the iterator design pattern in C#.

The BufferEnumerator class is the concrete iterator. MoveNext() advances the internal index and returns true if there are more elements to visit. Current computes the actual array index based on the ring buffer's wrap-around logic and returns the element at that position. Reset() moves the enumerator back to its initial state, and Dispose() is required by IEnumerator<T> to support resource cleanup -- though our implementation has no unmanaged resources to release.

The FixedSizeBuffer<T> class is the concrete aggregate. Its GetEnumerator() method creates and returns a new BufferEnumerator instance. It also implements the non-generic IEnumerable.GetEnumerator() to satisfy the interface hierarchy, which just delegates to the generic version.

Consuming the Collection

With the iterator pattern in place, our custom ring buffer works with foreach just like any built-in collection:

var buffer = new FixedSizeBuffer<string>(3);

buffer.Add("alpha");
buffer.Add("bravo");
buffer.Add("charlie");
buffer.Add("delta"); // overwrites "alpha"

foreach (string item in buffer)
{
    Console.WriteLine(item);
}

// Output:
// bravo
// charlie
// delta

The foreach loop calls GetEnumerator(), then repeatedly calls MoveNext() and reads Current until the buffer is exhausted. The consumer doesn't need to know anything about ring buffer indexing or wrap-around logic -- it just sees a sequence of strings. That's the whole point of the iterator design pattern.

Using yield return for Simplified Iterators

Writing a full enumerator class like BufferEnumerator above involves a fair amount of boilerplate. The iterator design pattern in C# gets significantly simpler with the yield return keyword, which eliminates that ceremony entirely. When you use yield return inside a method that returns IEnumerable<T>, the compiler generates a state machine class behind the scenes that implements IEnumerator<T> for you.

Here's what our ring buffer's GetEnumerator() method could look like using yield return:

public IEnumerator<T> GetEnumerator()
{
    for (int i = 0; i < _count; i++)
    {
        int actualIndex =
            (_head - _count + i + _items.Length)
            % _items.Length;

        yield return _items[actualIndex];
    }
}

That single method replaces the entire BufferEnumerator class. The compiler handles MoveNext(), Current, Dispose(), and all the state tracking automatically. Each time execution hits yield return, the method effectively pauses, returns the value, and resumes from that exact point when MoveNext() is called again.

You can also use yield break to terminate iteration early. This is useful when your iterator needs to stop based on a condition:

public static IEnumerable<int> TakeWhilePositive(
    IEnumerable<int> source)
{
    foreach (int value in source)
    {
        if (value <= 0)
        {
            yield break;
        }

        yield return value;
    }
}

When yield break is reached, the generated enumerator's MoveNext() returns false, and any finally blocks or Dispose() logic runs normally. This gives you clean early-exit semantics without managing state flags manually.

The trade-off with yield return is that you lose some control. You can't implement custom Reset() logic (the compiler-generated enumerator throws NotSupportedException), and the generated state machine allocates an object on the heap. For the vast majority of cases, these trade-offs are well worth the simplicity. Hand-written enumerators are typically reserved for performance-critical hot paths or collections with complex traversal logic that doesn't map cleanly to a simple loop, much like when you'd choose between hand-rolled logic and the template method design pattern for algorithmic flexibility.

Custom Iteration Patterns

The iterator design pattern in C# isn't limited to simple front-to-back traversal. By combining yield return with different traversal logic, you can create iterators that filter, reverse, or walk complex data structures -- all while presenting a clean IEnumerable<T> to the consumer.

Filtered Iteration

A filtered iterator yields only elements that satisfy a predicate. This is one of the simplest extensions of the iterator design pattern in C#:

public static IEnumerable<T> WhereCustom<T>(
    IEnumerable<T> source,
    Func<T, bool> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

This is essentially what LINQ's Where method does under the hood. You can consume it naturally:

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };

foreach (int n in WhereCustom(numbers, x => x % 2 == 0))
{
    Console.WriteLine(n);
}

// Output: 2, 4, 6, 8

Reverse Iteration

For collections that support indexed access, a reverse iterator walks backward:

public static IEnumerable<T> Reverse<T>(IList<T> source)
{
    for (int i = source.Count - 1; i >= 0; i--)
    {
        yield return source[i];
    }
}

Tree Traversal

Where the iterator pattern really shines is in traversing non-linear data structures. Consider a simple tree node:

public sealed class TreeNode<T>
{
    public T Value { get; }

    public List<TreeNode<T>> Children { get; }

    public TreeNode(T value)
    {
        Value = value;
        Children = new List<TreeNode<T>>();
    }

    public IEnumerable<T> DepthFirst()
    {
        yield return Value;

        foreach (TreeNode<T> child in Children)
        {
            foreach (T descendant in child.DepthFirst())
            {
                yield return descendant;
            }
        }
    }

    public IEnumerable<T> BreadthFirst()
    {
        var queue = new Queue<TreeNode<T>>();
        queue.Enqueue(this);

        while (queue.Count > 0)
        {
            TreeNode<T> current = queue.Dequeue();
            yield return current.Value;

            foreach (TreeNode<T> child in current.Children)
            {
                queue.Enqueue(child);
            }
        }
    }
}

Both DepthFirst() and BreadthFirst() return IEnumerable<T>, so the consumer can use foreach without knowing which traversal strategy is being used. The depth-first iterator uses recursive yield return to walk down each branch before moving to the next sibling. The breadth-first iterator uses a queue to visit all nodes at each level before descending. This is a great example of how the iterator design pattern works alongside structural patterns like the composite design pattern -- composites define the structure, and iterators define how you walk it.

You can consume either traversal identically:

var root = new TreeNode<string>("root");
var child1 = new TreeNode<string>("child-1");
var child2 = new TreeNode<string>("child-2");
child1.Children.Add(new TreeNode<string>("grandchild-1"));
child1.Children.Add(new TreeNode<string>("grandchild-2"));
root.Children.Add(child1);
root.Children.Add(child2);

foreach (string value in root.BreadthFirst())
{
    Console.WriteLine(value);
}

Iterator Pattern with LINQ

LINQ is arguably the most prominent use of the iterator design pattern in C# -- and many developers use it every day without realizing they're relying on iterators. LINQ methods like Where, Select, Take, Skip, and SelectMany all return IEnumerable<T> and use yield return (or equivalent state machines) internally. This means LINQ queries don't execute when you write them. They execute when you iterate over the results.

This behavior is called deferred execution, and it's a direct consequence of how iterators work. Consider this chain:

var numbers = new List<int>
{
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};

IEnumerable<int> query = numbers
    .Where(x => x > 3)
    .Select(x => x * 2)
    .Take(4);

When this code runs, no filtering, transformation, or limiting has happened. The query variable holds an object that represents the intent to perform those operations. Actual execution happens only when you start pulling elements -- via foreach, ToList(), First(), or any other method that triggers iteration.

This has practical implications for working with large datasets and is one of the biggest advantages of the iterator design pattern in C#. If you have a million records and you use Take(10), the iterator processes only 10 elements and then stops. There's no intermediate list of a million filtered items sitting in memory. Each LINQ operator pulls from the previous one on demand, creating a lazy pipeline that processes elements one at a time.

You can write your own LINQ-like extension methods using the same pattern:

public static class EnumerableExtensions
{
    public static IEnumerable<T> WhereNot<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate)
    {
        foreach (T item in source)
        {
            if (!predicate(item))
            {
                yield return item;
            }
        }
    }

    public static IEnumerable<(T Item, int Index)> WithIndex<T>(
        this IEnumerable<T> source)
    {
        int index = 0;

        foreach (T item in source)
        {
            yield return (item, index);
            index++;
        }
    }
}

These extension methods compose naturally with existing LINQ operators because they follow the same iterator design pattern in C#:

var results = names
    .WhereNot(n => string.IsNullOrWhiteSpace(n))
    .WithIndex()
    .Select(pair => $"{pair.Index}: {pair.Item}");

Understanding that LINQ is built on the iterator design pattern helps you write more efficient code. When you chain LINQ methods, you're building a pipeline of iterators -- not creating intermediate collections. This is the same principle that makes the chain of responsibility design pattern effective: each link in the chain processes its input and passes the result along, with no unnecessary buffering in between.

When to Use the Iterator Design Pattern

The iterator design pattern in C# is the right tool when you need to decouple traversal logic from data structure internals. Here are the scenarios where it provides the most value.

Custom data structures benefit heavily from implementing IEnumerable<T>. If you build a specialized collection -- a priority queue, a skip list, a spatial index -- implementing the iterator pattern lets consumers use foreach and LINQ without understanding your internal storage. This is exactly what we demonstrated with the ring buffer earlier.

Lazy evaluation of large datasets is another strong use case. When you're working with millions of records from a database, a file, or an API, you don't want to load everything into memory at once. An iterator that uses yield return produces elements on demand, keeping memory consumption proportional to what the consumer actually needs rather than the total dataset size.

Streaming data fits naturally into the iterator model. Reading lines from a log file, processing records from a CSV, or consuming events from a message queue -- all of these scenarios involve producing data incrementally and processing it as it arrives. The iterator pattern gives you a clean abstraction over the streaming source.

There are also cases where implementing the iterator design pattern is unnecessary. If you're working with a List<T>, an array, or any built-in .NET collection, it already implements IEnumerable<T> -- just use foreach directly. You don't need a custom iterator for structures that already have one. The pattern adds value when you're building something new, not when you're consuming something that already exists. Similarly, if you need random access to elements by index, the iterator pattern's sequential-access model isn't the right fit. Use IList<T> or array indexing instead.

Thinking about how the iterator pattern fits into the broader design pattern landscape helps too. It pairs well with the composite design pattern for traversing tree structures, the strategy design pattern for swappable traversal algorithms, and dependency injection via IServiceCollection for wiring up custom enumerators through your DI container. You can also look at the flyweight design pattern when your iterator produces many similar objects and memory efficiency matters.

Frequently Asked Questions

What is the iterator design pattern in C#?

The iterator design pattern is a behavioral design pattern that provides a way to access elements of a collection sequentially without exposing the collection's underlying representation. In C#, this pattern is implemented through the IEnumerable<T> and IEnumerator<T> interfaces. Any class that implements IEnumerable<T> can be used with foreach loops and LINQ methods, making the iterator pattern one of the most pervasive patterns in the .NET ecosystem.

How does foreach use the iterator pattern?

The C# compiler transforms a foreach loop into calls against the iterator interfaces. It calls GetEnumerator() on the collection to obtain an IEnumerator<T>, then enters a while loop that calls MoveNext() and reads Current on each iteration. When MoveNext() returns false, the loop ends. The compiler also generates a try/finally block to call Dispose() on the enumerator, ensuring proper cleanup of any resources.

What is the difference between IEnumerable and IEnumerator?

IEnumerable<T> represents a collection that can be iterated -- it's the aggregate role in the iterator pattern. Its only job is to create an IEnumerator<T> through GetEnumerator(). IEnumerator<T> represents the iteration state -- it tracks the current position and provides MoveNext() to advance and Current to access the element. Think of IEnumerable<T> as the book and IEnumerator<T> as the bookmark.

How does yield return work in C#?

The yield return keyword tells the compiler to generate a state machine class that implements IEnumerator<T> automatically. When execution reaches yield return, the method returns the value to the caller and suspends. The next call to MoveNext() resumes execution from exactly where it left off. This eliminates the need to write a separate enumerator class with manual state tracking. You can use yield break to terminate the iteration early.

What is deferred execution in LINQ?

Deferred execution means that a LINQ query doesn't run when you define it -- it runs when you iterate over the results. This is a direct consequence of the iterator design pattern in C#. LINQ methods like Where, Select, and Take return IEnumerable<T> objects backed by iterators. These iterators don't process any elements until a consuming operation (like foreach, ToList(), or First()) triggers iteration. This lazy behavior allows LINQ pipelines to process only the elements that are actually needed, which is especially important for large datasets.

Can I create multiple enumerators for the same collection?

Yes. Each call to GetEnumerator() should return an independent enumerator instance with its own position state. This means you can have multiple foreach loops iterating over the same collection simultaneously, each tracking its own position. This is why the enumerator is a separate object from the collection -- if the collection tracked position itself, you could only have one active traversal at a time.

When should I write a custom enumerator instead of using yield return?

Write a custom enumerator class when you need fine-grained control over allocation, reset behavior, or disposal logic. The compiler-generated state machine from yield return allocates heap objects and throws NotSupportedException from Reset(). In performance-critical code paths where you're iterating millions of times, a hand-written struct enumerator that avoids heap allocation can make a measurable difference. For most application code where the iterator design pattern in C# doesn't sit on a hot path, yield return is simpler and perfectly sufficient.

Wrapping Up the Iterator Design Pattern in C#

The iterator design pattern in C# is deeply woven into the language and framework. Every foreach loop, every LINQ query, and every yield return method relies on the same fundamental mechanism: an aggregate that creates an iterator, and an iterator that steps through elements one at a time. Understanding this pattern helps you see what's happening behind the abstractions you use every day.

When you need to expose traversal over a custom data structure, IEnumerable<T> and yield return make it straightforward. When you need lazy evaluation or streaming behavior, iterators give you that for free. When you need specialized traversal -- filtering, reversing, or walking a tree -- the pattern adapts cleanly through composition with other patterns.

Start by looking at places in your codebase where you're manually managing indexes or loading entire datasets into memory. These are opportunities to introduce iterators that simplify the code and improve efficiency. And if you're building design-pattern-literate C# applications, understanding how the iterator pattern connects to other behavioral patterns like the observer pattern and the command pattern gives you a broader vocabulary for solving recurring design problems.

Iterators - An Elementary Perspective on How They Function

Template Method Design Pattern in C#: Complete Guide with Examples

Master the template method design pattern in C# with practical examples showing inheritance-based algorithm customization and real-world .NET implementations.

Chain of Responsibility Design Pattern in C#: Complete Guide with Examples

Master the chain of responsibility design pattern in C# with practical examples showing handler chains, middleware pipelines, and real-world .NET implementations.

An error has occurred. This application may no longer respond until reloaded. Reload