Skip to main content

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();
}
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
tip

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​

IteratorVisitor
PurposeTraverse elementsAdd operations to elements
FocusNavigationProcessing
CombinationsN traversals Γ— 1 operation1 traversal Γ— N operations

.NET Real-World Usage​

  • IEnumerable<T> / IEnumerator<T> β€” the foundation of LINQ and foreach
  • yield return β€” compiler generates iterator state machines
  • LINQ operators β€” Where, Select, OrderBy are 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 foreach loop suffices
  • There's only one way to traverse β€” C#'s built-in IEnumerable is 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> and foreach
  • yield return makes 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()).

  • 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