Learnixo

.NET & C# Development · Lesson 46 of 229

Iterator — Traverse Collections Without Exposure

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

C#
// 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

C#
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, CFO

IAsyncEnumerable (C# 8+)

C#
// 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

C#
// 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 memory

Interview 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 use yield return to create lazy sequences. yield return creates 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 foreach processes 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, Take all return IEnumerable<T> iterators that are composed lazily and evaluated only when enumerated. Pitfall: evaluating the same LINQ query multiple times re-runs it — call ToList() to materialise when you need to reuse."