Iterator — Traverse Collections Without Exposure
The Iterator pattern in C#: implement IEnumerable and IEnumerator for custom collections, use yield return for lazy sequences, and build infinite generators.
Iterator — Traverse Collections Without Exposure
The Iterator pattern provides a way to access elements of a collection sequentially without exposing its internal structure. In C#, this is built into the language via IEnumerable<T>, IEnumerator<T>, and yield return.
Built-In: IEnumerable + yield return
// yield return creates an iterator automatically — no class needed
public IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true) // infinite sequence
{
yield return a;
(a, b) = (b, a + b);
}
}
// Take only what you need from the infinite sequence
foreach (int n in Fibonacci().Take(10))
Console.Write($"{n} ");
// 0 1 1 2 3 5 8 13 21 34
// yield break — early termination
public IEnumerable<string> ReadLines(string path)
{
using var reader = new StreamReader(path);
string? line;
while ((line = reader.ReadLine()) is not null)
yield return line;
// yield break is implicit at end of method
}Custom Iterator for a Tree
public class TreeNode<T>
{
public T Value { get; init; } = default!;
public List<TreeNode<T>> Children { get; } = new();
}
public class Tree<T> : IEnumerable<T>
{
private readonly TreeNode<T> _root;
public Tree(TreeNode<T> root) => _root = root;
// Depth-first traversal via yield
public IEnumerator<T> GetEnumerator()
{
foreach (var value in DepthFirst(_root))
yield return value;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
=> GetEnumerator();
private static IEnumerable<T> DepthFirst(TreeNode<T> node)
{
yield return node.Value;
foreach (var child in node.Children)
foreach (var value in DepthFirst(child))
yield return value;
}
// Alternative traversal — callers can choose
public IEnumerable<T> BreadthFirst()
{
var queue = new Queue<TreeNode<T>>();
queue.Enqueue(_root);
while (queue.Count > 0)
{
var node = queue.Dequeue();
yield return node.Value;
foreach (var child in node.Children)
queue.Enqueue(child);
}
}
}
// Build and traverse
var root = new TreeNode<string> { Value = "CEO" };
var cto = new TreeNode<string> { Value = "CTO" };
cto.Children.Add(new TreeNode<string> { Value = "Dev Lead" });
root.Children.Add(cto);
root.Children.Add(new TreeNode<string> { Value = "CFO" });
var tree = new Tree<string>(root);
foreach (string name in tree)
Console.WriteLine(name);
// CEO, CTO, Dev Lead, CFOIAsyncEnumerable (C# 8+)
// Async iterator — yields results as they become available
public async IAsyncEnumerable<Order> GetOrdersPagedAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
int page = 1;
while (true)
{
var orders = await _db.Orders
.Skip((page - 1) * 100)
.Take(100)
.ToListAsync(ct);
if (orders.Count == 0) yield break;
foreach (var order in orders)
yield return order;
page++;
}
}
// Caller processes one at a time — never loads all into memory
await foreach (var order in GetOrdersPagedAsync())
await ProcessOrderAsync(order);LINQ as Iterator Pattern
// All LINQ extension methods return IEnumerable<T> — lazy iterators
// Nothing executes until you enumerate
var query = Enumerable.Range(1, 1_000_000)
.Where(n => n % 2 == 0) // lazy — not evaluated yet
.Select(n => n * n) // lazy — not evaluated yet
.Take(10); // still lazy
// Evaluation happens here — only processes enough elements to get 10 results
foreach (int n in query)
Console.WriteLine(n);
// Pitfall: multiple enumerations re-evaluate the query
// Cache with ToList() or ToArray() when re-using
var cached = query.ToList(); // evaluated once, stored in memoryInterview Answer
"The Iterator pattern provides sequential access to a collection without exposing its internal structure. In C#, it's built into the language: implement
IEnumerable<T>and useyield returnto create lazy sequences.yield returncreates a state machine that resumes from where it left off each time the caller requests the next item — it's zero-allocation for small sequences and works with infinite streams.IAsyncEnumerable<T>(C# 8) extends this to async data:await foreachprocesses items as they arrive from a database page-by-page, never loading the full dataset into memory. LINQ is built entirely on the iterator pattern —Where,Select,Takeall returnIEnumerable<T>iterators that are composed lazily and evaluated only when enumerated. Pitfall: evaluating the same LINQ query multiple times re-runs it — callToList()to materialise when you need to reuse."
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.