Iterator Pattern
Definitionβ
The Iterator provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Coffee Shop Exampleβ
Our Coffee Shop has a menu stored as a Dictionary (for fast lookup by name). But customers want to browse the menu in different orders β by category, by price (cheapest first), or by popularity. We create custom iterators for each traversal strategy.
Structureβ
Iterator Interfaceβ
public interface IIterator<T>
{
bool HasNext();
T Next();
void Reset();
}
Menu Itemsβ
public enum Category { Coffee, Pastry, Beverage }
public record MenuItem(string Name, Category Category, decimal Price, int Popularity);
Concrete Iteratorsβ
public class MenuByCategoryIterator : IIterator<MenuItem>
{
private readonly List<MenuItem> _items;
private int _index;
public MenuByCategoryIterator(Dictionary<string, MenuItem> source)
{
// Sort by category, then by name
_items = source.Values
.OrderBy(i => i.Category)
.ThenBy(i => i.Name)
.ToList();
}
public bool HasNext() => _index < _items.Count;
public MenuItem Next() => _items[_index++];
public void Reset() => _index = 0;
}
public class MenuByPriceIterator : IIterator<MenuItem>
{
private readonly List<MenuItem> _items;
private int _index;
public MenuByPriceIterator(Dictionary<string, MenuItem> source)
{
_items = source.Values.OrderBy(i => i.Price).ToList();
}
public bool HasNext() => _index < _items.Count;
public MenuItem Next() => _items[_index++];
public void Reset() => _index = 0;
}
public class MenuByPopularityIterator : IIterator<MenuItem>
{
private readonly List<MenuItem> _items;
private int _index;
public MenuByPopularityIterator(Dictionary<string, MenuItem> source)
{
_items = source.Values.OrderByDescending(i => i.Popularity).ToList();
}
public bool HasNext() => _index < _items.Count;
public MenuItem Next() => _items[_index++];
public void Reset() => _index = 0;
}
Aggregate β The Coffee Menuβ
public class CoffeeMenu
{
private readonly Dictionary<string, MenuItem> _items = new();
public void Add(MenuItem item) => _items[item.Name] = item;
public IIterator<MenuItem> CreateCategoryIterator() =>
new MenuByCategoryIterator(_items);
public IIterator<MenuItem> CreatePriceIterator() =>
new MenuByPriceIterator(_items);
public IIterator<MenuItem> CreatePopularityIterator() =>
new MenuByPopularityIterator(_items);
}
Client β Traversing the Menuβ
var menu = new CoffeeMenu();
menu.Add(new("Latte", Category.Coffee, 4.00m, 95));
menu.Add(new("Espresso", Category.Coffee, 2.50m, 80));
menu.Add(new("Croissant", Category.Pastry, 3.00m, 60));
menu.Add(new("Cold Brew", Category.Coffee, 3.50m, 90));
menu.Add(new("Muffin", Category.Pastry, 3.50m, 50));
menu.Add(new("Orange Juice", Category.Beverage, 4.00m, 40));
// Browse by category
Console.WriteLine("=== Menu by Category ===");
var categoryIter = menu.CreateCategoryIterator();
while (categoryIter.HasNext())
{
var item = categoryIter.Next();
Console.WriteLine($" {item.Category,-10} {item.Name,-15} ${item.Price:F2}");
}
// Browse by price (cheapest first)
Console.WriteLine("\n=== Menu by Price ===");
var priceIter = menu.CreatePriceIterator();
while (priceIter.HasNext())
{
var item = priceIter.Next();
Console.WriteLine($" ${item.Price:F2} {item.Name}");
}
// Browse by popularity
Console.WriteLine("\n=== Most Popular ===");
var popIter = menu.CreatePopularityIterator();
while (popIter.HasNext())
{
var item = popIter.Next();
Console.WriteLine($" #{item.Popularity} {item.Name}");
}
// Output:
// === Menu by Category ===
// Coffee Latte $4.00
// Coffee Espresso $2.50
// Coffee Cold Brew $3.50
// Pastry Croissant $3.00
// Pastry Muffin $3.50
// Beverage Orange Juice $4.00
//
// === Menu by Price ===
// $2.50 Espresso
// $3.00 Croissant
// $3.50 Cold Brew
// $3.50 Muffin
// $4.00 Latte
// $4.00 Orange Juice
//
// === Most Popular ===
// #95 Latte
// #90 Cold Brew
// #80 Espresso
// #60 Croissant
// #50 Muffin
// #40 Orange Juice
The internal storage (Dictionary<string, MenuItem>) is hidden. Clients traverse the menu through iterators without knowing how items are stored. Different iterators provide different views of the same data.
C# Built-In: IEnumerable and foreachβ
C# has Iterator built into the language via IEnumerable<T> and yield return:
// Modern C# β no custom iterator class needed
public class CoffeeMenu : IEnumerable<MenuItem>
{
private readonly List<MenuItem> _items = new();
public void Add(MenuItem item) => _items.Add(item);
// By price
public IEnumerable<MenuItem> ByPrice() =>
_items.OrderBy(i => i.Price);
// By category
public IEnumerable<MenuItem> ByCategory() =>
_items.OrderBy(i => i.Category).ThenBy(i => i.Name);
// Default iteration (required for IEnumerable<T>)
public IEnumerator<MenuItem> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// Usage with foreach
foreach (var item in menu.ByPrice())
Console.WriteLine($"{item.Name}: ${item.Price:F2}");
Iterator vs Visitorβ
| Iterator | Visitor | |
|---|---|---|
| Purpose | Traverse elements | Add operations to elements |
| Focus | Navigation | Processing |
| Combinations | N traversals Γ 1 operation | 1 traversal Γ N operations |
.NET Real-World Usageβ
IEnumerable<T>/IEnumerator<T>β the foundation of LINQ andforeachyield returnβ compiler generates iterator state machines- LINQ operators β
Where,Select,OrderByare iterator combinators IAsyncEnumerable<T>β asynchronous iteration (await foreach)Span<T>/Memory<T>β efficient iteration over memory regions
When to Useβ
- You need to traverse a collection without exposing its internal structure
- You need multiple traversal strategies for the same collection
- You want a uniform traversal interface across different collection types
When NOT to Useβ
- The collection is simple and a
foreachloop suffices - There's only one way to traverse β C#'s built-in
IEnumerableis enough - You need random access β use an index instead
Key Takeawaysβ
- Iterator provides sequential access without exposing internal structure
- Multiple iterators can provide different traversal orders over the same data
- C# has Iterator deeply integrated via
IEnumerable<T>andforeach yield returnmakes custom iterators trivial to implement
Interview Questionsβ
Q: What's the difference between IEnumerable<T> and IEnumerator<T>?
IEnumerable<T> is the aggregate β it has GetEnumerator() which returns an IEnumerator<T>. IEnumerator<T> is the iterator β it has MoveNext(), Current, and Reset(). foreach calls GetEnumerator() under the hood.
Q: How does yield return work?
The compiler transforms a yield return method into a state machine class that implements IEnumerator<T>. Each yield return is a suspension point. The generated class tracks state between calls to MoveNext().
Q: What's the difference between eager and lazy iteration?
List<T> is eager β all elements are in memory. IEnumerable<T> with yield return is lazy β elements are produced on demand. LINQ is lazy by default β nothing executes until you enumerate (e.g., with foreach or .ToList()).
Related Topicsβ
- Visitor β adds operations while iterating
- Composite β iterators often traverse composite trees
- LINQ β C#'s built-in iterator pipeline
- Collections β
IEnumerable<T>is the base of all collections