📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do iterators work in C#? Explain yield return and yield break, what state machine the compiler generates, lazy evaluation, how try/finally and using behave inside iterators, the restrictions on iterator methods, async iterators, and the performance trade-offs of yield.

Iterators: yield return and yield break semantics, compiler-generated state machines, disposal and finally, restrictions (no ref params, no yield in catch), async iterators, and cost versus materialised collections.

Mid-Level Theory Advanced async-streams csharp iterators state-machine yield

🎯 Key points

  • A method with `yield return` returning IEnumerable/IEnumerator (or IAsyncEnumerable) is an iterator; the compiler rewrites it into a state machine class
  • `yield return x` produces one element and pauses; `yield break` ends the sequence (like return for iterators)
  • Locals become fields; a `state` field records where to resume on the next MoveNext
  • Execution is lazy and on demand; each GetEnumerator gives an independent run
  • `finally` and `using` blocks run when iteration completes or when the consumer disposes the enumerator early
  • Restrictions: no ref/out/in parameters, no `yield` inside a catch block or in a try that has a catch, no plain return of a value
  • Async iterators (C# 8) combine await and yield and return IAsyncEnumerable<T>
  • Iterators allocate a state machine and call through interfaces; for small, fully needed results a List may be cheaper

⏱ 30-second answer

A method that uses yield return is an iterator, and the compiler rewrites it into a state machine class that implements IEnumerable<T> and IEnumerator<T>. Locals become fields, a state field remembers where it paused, and each MoveNext runs to the next yield return. yield break ends the sequence. Execution is lazy, and finally and using blocks run when iteration finishes or the consumer disposes early. Iterators can't have ref or out parameters or yield inside a catch, and async iterators add await. They're ideal for streaming and large or infinite sequences.

🗣 Full answer (senior)

How I would answer this in an interview

An iterator is a method (or property/operator) using yield return/yield break that returns IEnumerable<T>/IEnumerator<T> (or IAsyncEnumerable<T>). The compiler rewrites it into a state machine class that produces values lazily, one per MoveNext(). I'd cover the keywords, the generated state machine, lazy evaluation, try/finally and using, restrictions, async iterators, and performance trade-offs.

1. yield return and yield break

public static IEnumerable<OrderLine> ParseLines(TextReader reader)
{
    string? line;
    while ((line = reader.ReadLine()) is not null)
    {
        if (line.StartsWith("#")) continue;      // skip comments
        if (line == "END") yield break;          // stop early
        yield return OrderLine.Parse(line);      // produce one item, pause here
    }
}
  • yield return x produces x and pauses the method until the next MoveNext().
  • yield break ends the sequence.
  • Execution resumes right after the last yield return, with all locals intact.

2. The generated state machine

The compiler generates a private class implementing IEnumerable<T> and IEnumerator<T> with:

  • an integer state field (where to resume),
  • the current value,
  • hoisted locals and parameters as fields,
  • MoveNext() containing the method body transformed into a switch on state,
  • Dispose() that runs pending finally blocks,
  • GetEnumerator() that returns this the first time (same thread) or a fresh clone afterwards, so each enumeration restarts from the beginning.

Calling ParseLines(reader) only allocates this object — no code in the body runs until enumeration.

3. Lazy evaluation

  • Items are produced on demand; consumers can stop early (First(), Take(10), break), and the rest of the body never runs.
  • Infinite sequences are possible (while (true) yield return ...).
  • Memory stays constant for streaming (process a 10 GB file line by line).
  • But side effects and exceptions happen during enumeration, and each enumeration re-runs the method (re-reading files, re-querying).

4. try/finally and using inside iterators

public static IEnumerable<string> ReadLines(string path)
{
    using var reader = new StreamReader(path);   // disposed when enumeration completes OR the consumer disposes early
    string? line;
    while ((line = reader.ReadLine()) is not null) yield return line;
}
  • finally blocks (including those generated by using) run when iteration completes, when an exception escapes, or when the consumer disposes the enumerator (which foreach does in its own finally, including on break).
  • If a consumer enumerates manually and never calls Dispose(), the file stays open until GC.
  • You can't yield return inside a catch block or inside a finally block; you can yield return inside a try that has only a finally (and since C# 13 some restrictions around try/catch remain — you can't yield inside a try that has a catch).

5. Restrictions on iterator methods

  • No ref, out or in parameters (they'd need to outlive the call).
  • Return type must be IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T>, or async equivalents.
  • Can't contain return with a value (only yield return/yield break).
  • Before C# 13, iterators couldn't use ref locals or ref struct locals (like Span<T>); C# 13 allows them as long as they don't cross a yield return.
  • Unsafe code restrictions apply.

6. Async iterators

public async IAsyncEnumerable<Customer> StreamCustomersAsync([EnumeratorCancellation] CancellationToken ct = default)
{
    string? continuation = null;
    do
    {
        var page = await _api.GetCustomersAsync(continuation, ct);
        foreach (var c in page.Items) yield return c;
        continuation = page.Continuation;
    } while (continuation is not null);
}

await foreach (var c in client.StreamCustomersAsync().WithCancellation(ct)) { ... }

They combine await and yield return, producing an IAsyncEnumerable<T> — perfect for paging APIs and streaming database results without loading everything.

7. Performance trade-offs

  • Costs: one state-machine allocation per enumeration, interface calls for MoveNext/Current, and no inlining across the boundary. In tight loops over small in-memory collections, a plain loop or returning an array/span is faster.
  • Benefits: streaming (constant memory), early termination, composability with LINQ, simpler code than hand-written enumerators.
  • For hot paths, consider a struct enumerator pattern (hand-written) or Span<T>-based APIs.

8. A production example

A nightly export loaded 3 million rows into a List<T> before writing CSV, peaking at several GB and triggering container OOM kills. Rewriting the data access as an async iterator (AsAsyncEnumerable() from EF Core with AsNoTracking) and writing each row to the response stream as it arrived kept memory flat at tens of MB. We made sure the iterator used await using for the connection so early cancellation released resources.

🔍 What the interviewer is checking

Checks a core advanced C# mechanism.

Green flags: state machine with state and current fields; yield return versus yield break; lazy evaluation; finally on Dispose; restrictions; async iterators; trade-offs.

Red flags: thinks yield returns a list; does not know cleanup semantics; cannot explain why validation is deferred.

📚 Learn in depth

1. State machine sketch

private sealed class <ParseLines>d__0 : IEnumerable<OrderLine>, IEnumerator<OrderLine>
{
    private int <>1__state;            // -2: not started (enumerable), 0: start, 1: after yield, -1: finished
    private OrderLine <>2__current;
    private int <>l__initialThreadId;
    private TextReader reader;          // parameter copy
    private string? <line>5__1;         // hoisted local

    public bool MoveNext()
    {
        switch (<>1__state)
        {
            case 0: <>1__state = -1; goto loop;
            case 1: <>1__state = -1; goto loop;   // resume after yield
            default: return false;
        }
    loop:
        while ((<line>5__1 = reader.ReadLine()) != null)
        {
            if (<line>5__1.StartsWith("#")) continue;
            if (<line>5__1 == "END") return false;
            <>2__current = OrderLine.Parse(<line>5__1);
            <>1__state = 1;
            return true;
        }
        return false;
    }
}

With try/finally, states encode "inside try" so Dispose() knows which finally blocks to run.

2. GetEnumerator reuse

The same object implements both interfaces. If GetEnumerator() is called on the thread that created it and it hasn't started, it returns this (saving an allocation); otherwise it creates a new instance with copied parameters. That's why enumerating twice restarts the method.

3. Async iterator internals

Async iterators generate a class implementing IAsyncEnumerable<T>, IAsyncEnumerator<T>, and IValueTaskSource<bool>/IValueTaskSource, using ManualResetValueTaskSourceCore<bool> to avoid allocations per MoveNextAsync. Cancellation flows via [EnumeratorCancellation]; DisposeAsync runs finally blocks, including await using disposals.

4. Exceptions inside iterators

Exceptions thrown in the body propagate out of MoveNext() to the consumer, at enumeration time. You can't wrap yield return in try/catch, so error handling for per-item failures usually goes in a helper that returns a result (e.g. TryParse) and the iterator yields valid items or error records.

5. Iterators vs materialised collections

Need Choose
Streaming large/unbounded data Iterator / async iterator
Multiple passes, Count, indexing Materialised List<T>/array
Hot tight loop over small data Array/span, plain loops
Lazy composition with LINQ Iterator

Try it yourself

  1. Add logging inside an iterator and observe when it runs relative to the call and foreach.
  2. Use using inside an iterator and break after the first item; confirm disposal.
  3. Write an async iterator over a paged API with cancellation.
  4. Benchmark an iterator vs returning an array for summing 100 small items.

💻 Code

using System.Runtime.CompilerServices;

namespace Advanced.Iterators;

public sealed record LogEntry(DateTimeOffset Timestamp, string Level, string Message);

public static class LogReader
{
    // Streams a huge file one line at a time
    public static IEnumerable<LogEntry> Read(string path)
    {
        ArgumentException.ThrowIfNullOrEmpty(path);
        return Iterator(path);

        static IEnumerable<LogEntry> Iterator(string path)
        {
            using var reader = new StreamReader(path);
            try
            {
                while (reader.ReadLine() is { } line)
                {
                    if (line.Length == 0) continue;
                    if (line == "--END--") yield break;          // stop the sequence early

                    var parts = line.Split('|', 3);
                    if (parts.Length < 3) continue;                 // skip malformed lines
                    yield return new LogEntry(DateTimeOffset.Parse(parts[0]), parts[1], parts[2]);
                }
            }
            finally
            {
                Console.WriteLine("log reader disposed");          // runs even if the consumer stops early
            }
        }
    }

    // Infinite sequence: always combine with Take/First
    public static IEnumerable<int> RetryDelaysMs()
    {
        var delay = 100;
        while (true)
        {
            yield return delay;
            delay = Math.Min(delay * 2, 10_000);
        }
    }

    // Async iterator
    public static async IAsyncEnumerable<int> PollAsync(int times, [EnumeratorCancellation] CancellationToken ct = default)
    {
        for (var i = 1; i <= times; i++)
        {
            await Task.Delay(10, ct);
            yield return i;
        }
    }
}

public static class Program
{
    public static async Task Main()
    {
        var path = Path.GetTempFileName();
        await File.WriteAllLinesAsync(path,
        [
            "2026-10-23T10:00:00+11:00|INFO|start",
            "2026-10-23T10:00:01+11:00|ERROR|payment timeout",
            "--END--",
            "2026-10-23T10:00:02+11:00|INFO|never read",
        ]);

        var firstError = LogReader.Read(path).First(e => e.Level == "ERROR"); // stops early, finally runs
        Console.WriteLine(firstError.Message);

        Console.WriteLine(string.Join(",", LogReader.RetryDelaysMs().Take(6)));  // 100,200,400,800,1600,3200

        await foreach (var n in LogReader.PollAsync(3)) Console.WriteLine($"poll {n}");
    }
}

⚠ Common mistakes

1. Expecting the body to run at call time

What happens: validation or setup in iterator bodies. Why it is wrong: runs only on enumeration. Do instead: validate eagerly in a wrapper; iterate in a local function.

2. Enumerating iterators multiple times

What happens: Count() then foreach over an iterator reading a file or database. Why it is wrong: re-runs I/O each time. Do instead: materialise once if you need multiple passes.

3. Manual enumeration without disposal

What happens: GetEnumerator()/MoveNext() without Dispose. Why it is wrong: finally/using blocks don't run; resources leak. Do instead: use foreach or using var e.

4. Holding resources across slow consumers

What happens: iterators keeping database connections open while consumers do slow work per item. Why it is wrong: connection pool exhaustion and long-running transactions. Do instead: buffer appropriately or keep per-item work fast.

5. Using iterators in hot tight loops

What happens: iterator methods over tiny collections called millions of times. Why it is wrong: allocation and interface call overhead. Do instead: arrays, spans or struct enumerators.

6. Trying to yield inside catch

What happens: wrapping yield return in try/catch. Why it is wrong: not allowed. Do instead: move error handling into helpers or yield result objects.

7. Forgetting [EnumeratorCancellation]

What happens: async iterators ignore WithCancellation tokens. Why it is wrong: can't cancel streaming operations. Do instead: annotate the token parameter.

8. Side effects in iterators

What happens: iterators that update state or send messages as they yield. Why it is wrong: effects depend on how far and how often consumers enumerate. Do instead: keep iterators pure or make effects explicit.

🏛 Senior insight: trade-offs

Iterators are the engine behind LINQ and streaming APIs. Seniors know the state-machine lowering well enough to predict behaviour: lazy start, recomputation on each enumeration, and cleanup through Dispose. I use them for streaming large or unbounded data and pipelines, pair them with eager validation wrappers, and choose materialised collections when the result is small or needs to be enumerated more than once.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions lazy evaluation. Why: Iterators are the engine of deferred IEnumerable sequences.
    How to answer

    IEnumerable<T> is just a promise that you can get an enumerator. IEnumerator<T> gives MoveNext, Current and Dispose, and that is all: no count, no indexing, and possibly lazy, infinite or single-use. foreach is pattern-based and compiles to a while (MoveNext()) loop with Dispose in a finally. List<T> uses a struct enumerator, which gets boxed if you iterate through the interface. IAsyncEnumerable<T> is the async version, used with await foreach. I accept IEnumerable<T> when I only iterate once, and return IReadOnlyList<T> unless I mean to stream.

  2. When: Candidate mentions argument checks inside an iterator. Why: Eager argument validation needs a wrapper or local function.
    How to answer

    An iterator method runs none of its body until the first MoveNext, so a ThrowIfNull at the top only fires when someone enumerates. An async method stores the exception in the returned task, so it only shows up when awaited. The fix is to keep the public method non-iterator and non-async: validate there, then return a call to a local iterator or local async function. That's exactly how LINQ validates eagerly. Local functions can capture variables without allocating when they aren't turned into delegates, and marking them static forbids capture altogether. I use them for private helpers that only make sense inside one method.

  3. When: Candidate mentions Where or Select being lazy. Why: LINQ to Objects operators are mostly iterators.
    How to answer

    LINQ to Objects is a library of extension methods on IEnumerable<T>. Where and Select take delegates and return iterator objects that pull items from their source only when you enumerate, so a query is a lazy pipeline. Query syntax is just compiled into those method calls. Operators like Where stream item by item, while OrderBy and GroupBy must buffer the source first. The BCL fuses common chains and uses Count when the source is a collection. Recent .NET versions added Chunk, MinBy, CountBy, Index, and in .NET 10 LeftJoin and RightJoin.

🛠 Real-world problems: how you handle them

Log analysis ran out of memory on large files
Situation
An internal incident-analysis tool loaded production log files of up to 20 GB with File.ReadAllLines before filtering.
Signals
OutOfMemoryException on large files and multi-minute delays before any output appeared.
Investigation
Watched memory with dotnet-counters during a run and saw the GC heap grow to the size of the file; the code materialised all lines and parsed entries into a List.
Resolution
Rewrote the reader as an iterator over StreamReader.ReadLine with yield return, so filtering and Take(N) stopped reading as soon as enough matches were found.
Prevention
Added a test with a generated multi-gigabyte file in the nightly build to catch regressions in memory use.

How to tell it (STAR)

**S:** Our log analysis tool crashed on large production logs during incidents, exactly when we needed it. **T:** Make it work on any file size. **A:** I replaced the load-everything approach with an iterator that yields one parsed entry at a time, so the pipeline streams and stops early. **R:** Memory stayed under [amount] for any file size and the first results appeared in seconds.

↩ Usually asked after

More in Advanced Language Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me