A logbook for curious waters
← Software Engineering Field Guide

Iterator

Before you begin: Collections, loops, and object state.

An Iterator keeps track of a traversal through a collection. Callers request the next element without managing the collection’s internal representation. The collection can provide independent iterators so two traversals do not overwrite each other’s position. 1

Two readers should not share one cursor

A party roster stores names. One part of the program may be listing members while another starts its own pass. Storing a single “current member” field on the roster would make those traversals interfere.

Instead, each traversal gets its own iterator. These examples capture the names at roster construction and do not expose mutation operations, keeping the iteration contract simple.

public sealed class Roster : IEnumerable<string>
{
    private readonly string[] names;

    public Roster(IEnumerable<string> names)
    {
        this.names = names.ToArray();
    }

    public IEnumerator<string> GetEnumerator()
    {
        foreach (string name in names)
        {
            yield return name;
        }
    }

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

IEnumerable<string> means the roster can supply an enumerator. yield return lets the compiler generate an enumerator that remembers where execution was suspended. Each GetEnumerator call produces an independent traversal. 2

var roster = new Roster(new[] { "Mira", "Sol" });
foreach (string name in roster) Console.WriteLine(name);

The loop obtains an enumerator, advances it, reads its current element, and disposes it when appropriate. It does not need access to the private array. The explicit nongeneric implementation satisfies the older enumeration interface too.

class Roster:
    def __init__(self, names):
        self._names = tuple(names)

    def __iter__(self):
        for name in self._names:
            yield name

Roster is an iterable: calling iter(roster) obtains an iterator. This generator method creates a fresh traversal each time. next(iterator) advances that traversal until it raises StopIteration. 3

roster = Roster(["Mira", "Sol"])
first = iter(roster)
second = iter(roster)
print(next(first))   # Mira
print(next(first))   # Sol
print(next(second))  # Mira

The second iterator begins at the first name even though the first iterator has already reached the second. A generator object itself is normally one-shot; obtaining another iterator from the same generator does not create an independent copy.

Traversal has a contract too

The roster visits names in insertion order and permits repeated names. It takes a snapshot at construction, so later changes to the input list do not affect either traversal. Other collections may provide live views or reject mutation during iteration. State that behavior instead of assuming every for loop means the same thing.

An iterator can generate values lazily, read files, or fetch pages. In those cases the next step may be expensive or fail. A familiar loop does not mean every value is already in memory. Resources and cancellation also need deliberate ownership.

Usually the language’s collection and generator facilities already provide the pattern. Write a custom iterator when you need a specific traversal contract, not because every array needs another wrapper. Its value is a stable way to visit elements while keeping traversal state separate from the collection and from other readers.

Sources

  1. Microsoft — Discovering the Design Patterns You’re Already Using in .NET, including Iterator.
  2. Microsoft — yield statement.
  3. Python — Iterator types.