Iterator Pattern
Overview
Callers that index directly into a playlist array depend on its storage representation. Separating the stored items from the current traversal position lets the collection hide those details.
An Iterator advances to the next item and exposes the current one. The collection holds items; each iterator has its own position.
UML
Playlist creates an iterator. Callers use IEnumerator<string>.MoveNext() and Current. The C# compiler turns yield return into the iterator implementation.
History
GoF cataloged Iterator as a behavioral pattern in Design Patterns in 1994. It provides sequential access without exposing the aggregate representation.1
Why use it?
Callers read items in order without receiving the internal array. Independent iterators can traverse the same collection at different positions.
Deferred traversal may perform I/O or fail during enumeration. This example reads a copied array of strings, not a changing collection or a remote stream.
Example
Enumerate Playlist with foreach without exposing its array.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
foreach (string track in playlist) Console.WriteLine(track);
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
intro
outroforeach uses the iterator protocol. C# yield return provides a concise way to write an implementation that supplies items one at a time.
An extended example
Create two iterators for the same playlist and advance them independently.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
using var first = playlist.GetEnumerator();
using var second = playlist.GetEnumerator();
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
if (second.MoveNext()) Console.WriteLine("second: " + second.Current);
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
Console.WriteLine("first has next: " + first.MoveNext());
Console.WriteLine("second: " + second.Current);
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
first: intro
second: intro
first: outro
first has next: False
second: introAdvancing the first iterator does not change the second Current value. Do not read Current after MoveNext() returns false.
Related reading
Each example is a separate .NET 10 console program.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Iterator. β©