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
foreachworks on anything that implementsIEnumerable<T>, whoseGetEnumerator()hands out a new iterator each time.- An iterator is an
IEnumerator<T>:MoveNext()steps to the next item and returnsfalsewhen there are no more;Currentis the item it's on. yield returnhands 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
positionstarts at-1, before the first item. The firstMoveNext()moves it to0.GetEnumeratormakes 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.GetEnumeratorandIEnumerator.Currentare there because the non-generic interfaces are older than generics; they just pass on to the real ones.Resetcomes from that old interface too (foreachnever calls it). Disposecomes fromIDisposable:foreachcalls 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.