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);
// Hide the internal array and expose only the traversal protocol.
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
// Each call returns a generator with its own traversal position.
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);
// Hide the internal array and expose only the traversal protocol.
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
// Each call returns a generator with its own traversal position.
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.
TypeScript example
Playlist owns the items; each iterator owns its current position. Keeping position out of the collection lets two traversals progress independently.
// Hide the internal array and expose only the traversal protocol.
class Playlist implements Iterable<string> {
private readonly tracks: readonly string[];
constructor(tracks: readonly string[]) { this.tracks = [...tracks]; }
// Each call returns a generator with its own traversal position.
*[Symbol.iterator](): Generator<string, void, unknown> {
for (const track of this.tracks) yield track;
}
}
function step(iterator: Iterator<string>): string {
const result = iterator.next();
return result.done ? "(end)" : result.value;
}
const playlist = new Playlist(["intro", "outro"]);
const first = playlist[Symbol.iterator]();
const second = playlist[Symbol.iterator]();
console.log("first: " + step(first));
console.log("first: " + step(first));
console.log("second: " + step(second));
console.log("second: " + step(second));first: intro
first: outro
second: intro
second: outroSymbol.iterator provides the entry point corresponding to C# GetEnumerator(). Each generator call creates a separate iterator; after traversal, check the done flag returned by next().
Use LINQ for filtering and transformation
An existing iterable needs no new iterator class just to filter and transform its items. C# LINQ composes traversal by receiving a predicate in Where and a transformation function in Select. This example uppercases only track names beginning with in and leaves the input array unchanged.
C#
using System;
using System.Linq;
var tracks = new[] { "intro", "outro", "interlude" };
// Where filters, Select transforms, and ToArray enumerates and stores the results now.
string[] labels = tracks
.Where(track => track.StartsWith("in", StringComparison.Ordinal))
.Select(track => track.ToUpperInvariant())
.ToArray();
Console.WriteLine(string.Join(",", labels));
Console.WriteLine(string.Join(",", tracks));TypeScript
const tracks = ["intro", "outro", "interlude"];
// Array filter and map evaluate immediately and return new arrays.
const labels = tracks
.filter(track => track.startsWith("in"))
.map(track => track.toUpperCase());
console.log(labels.join(","));
console.log(tracks.join(","));INTRO,INTERLUDE
intro,outro,interludeThese examples use strings in memory. LINQ Where and Select run during enumeration, with ToArray choosing when to enumerate; TypeScript array filter and map evaluate immediately. Both select and uppercase the same ASCII track names here without modifying the source. Functional syntax alone does not prevent external calls or shared-state mutation.
Microsoft: Introduction to LINQ queries
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Iterator. β©