Iterator

The Iterator pattern lets code walk through a collection without knowing how it's stored. In C#, foreach works on anything that implements IEnumerable<T>, which hands out an IEnumerator<T> with MoveNext and Current. Write one by hand to see how it works, then let yield return write it for you.

Journeyman · C# · OOP · Updated

After this lesson you can

  • Make a class work with foreach by implementing IEnumerable<T> with yield return

Key takeaways

  • foreach works on anything that implements IEnumerable<T>, whose GetEnumerator() hands out a new iterator each time.
  • An iterator is an IEnumerator<T>: MoveNext() steps to the next item and returns false when there are no more; Current is the item it's on.
  • yield return hands out one item and pauses until the loop asks for the next, so the compiler writes the iterator class for you.
  • A class that implements IEnumerable<T> works with LINQ too, and can still keep its list private.

A foreach loop works on a List, an array, a Dictionary, a string, and the results of a LINQ query. They store their items in completely different ways, yet foreach doesn't care. That's the Iterator pattern: a way to go through a collection, one item at a time, without knowing how it's stored.

A team that foreach can't loop over

A team holds at most four people. It keeps them in a private list, so nobody can add a fifth behind its back:

var team = new Team();
team.Add(new Person("Ada", "Databases"));
team.Add(new Person("Grace", "Compilers"));

public record Person(string Name, string Skill);

public class Team
{
    private readonly List<Person> people = new List<Person>();

    public void Add(Person person)
    {
        if (people.Count == 4)
        {
            throw new InvalidOperationException("A team has at most four people.");
        }
        people.Add(person);
    }
}

Now try foreach (Person person in team). It doesn't compile:

error CS1579: foreach statement cannot operate on variables of type 'Team' because 'Team' does not contain a public instance or extension definition for 'GetEnumerator'

What foreach needs

foreach needs the collection to give it an iterator: an object that walks through the items. C# describes both sides with two interfaces:

Interface Has Means
IEnumerable<T> GetEnumerator() "You can loop over me": hands out a new iterator
IEnumerator<T> MoveNext() and Current The iterator: MoveNext() steps to the next item, and says false when there are no more; Current is the item it's on

A foreach loop is shorthand for:

IEnumerator<Person> iterator = team.GetEnumerator();
while (iterator.MoveNext())
{
    Person person = iterator.Current;
    // the loop's body
}

So, to make Team work with foreach, it implements IEnumerable<Person>.

By hand

Here's an iterator written out in full, so you can see the parts:

using System.Collections;

var team = new Team();
team.Add(new Person("Ada", "Databases"));
team.Add(new Person("Grace", "Compilers"));

foreach (Person person in team)
{
    Console.WriteLine(quot;{person.Name}: {person.Skill}");
}

public record Person(string Name, string Skill);

public class Team : IEnumerable<Person>
{
    private readonly List<Person> people = new List<Person>();

    public void Add(Person person)
    {
        if (people.Count == 4)
        {
            throw new InvalidOperationException("A team has at most four people.");
        }
        people.Add(person);
    }

    public IEnumerator<Person> GetEnumerator() => new TeamIterator(people);

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class TeamIterator : IEnumerator<Person>
{
    private readonly List<Person> people;
    private int position = -1;

    public TeamIterator(List<Person> people)
    {
        this.people = people;
    }

    public bool MoveNext()
    {
        position++;
        return position < people.Count;
    }

    public Person Current => people[position];

    object IEnumerator.Current => Current;

    public void Reset() => position = -1;

    public void Dispose() { }
}
Ada: Databases
Grace: Compilers
  • position starts at -1, before the first item. The first MoveNext() moves it to 0.
  • GetEnumerator makes a new iterator each time. That matters: two loops over the same team, even one inside the other, each need their own place in the list.
  • The extra IEnumerable.GetEnumerator and IEnumerator.Current are there because the non-generic interfaces are older than generics; they just pass on to the real ones. Reset comes from that old interface too (foreach never calls it).
  • Dispose comes from IDisposable: foreach calls it when the loop ends, so an iterator can clean up. This one has nothing to clean up.

It works, but it's a lot of code for "go through the list".

With yield return

C# can write the iterator for you. In a method that returns IEnumerator<T> or IEnumerable<T>, yield return hands out one item, and pauses until the loop asks for the next:

using System.Collections;

var team = new Team();
team.Add(new Person("Ada", "Databases"));
team.Add(new Person("Grace", "Compilers"));
team.Add(new Person("Linus", "Kernels"));

foreach (Person person in team)
{
    Console.WriteLine(person.Name);
}

Console.WriteLine(string.Join(", ", team.Where(p => p.Name.Length > 3).Select(p => p.Name)));

public record Person(string Name, string Skill);

public class Team : IEnumerable<Person>
{
    private readonly List<Person> people = new List<Person>();

    public void Add(Person person)
    {
        if (people.Count == 4)
        {
            throw new InvalidOperationException("A team has at most four people.");
        }
        people.Add(person);
    }

    public IEnumerator<Person> GetEnumerator()
    {
        foreach (Person person in people)
        {
            yield return person;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Ada
Grace
Linus
Grace, Linus

The compiler turns that method into an iterator class like TeamIterator, with its position and MoveNext, behind the scenes. And because Team is an IEnumerable<Person>, LINQ works on it too: Where and Select only need something they can loop over.

The list stays private. Callers can loop over the team, but they can't Add a fifth person to the list, or remove anyone: that's encapsulation and the Iterator pattern working together.

Check yourself

1 question to try on the lesson page.

So what

foreach works on anything that implements IEnumerable<T>. Implement it with yield return, and your own classes get foreach and LINQ, while keeping how they store their items to themselves.

Key terms

Cheat sheets