Iterators and yield return

C# · Advanced · 12 min

Iterators are how LINQ and streaming code process large or infinite data without loading it all.

Anything you can foreach over implements IEnumerable<T>, which hands out an IEnumerator<T> with MoveNext() and Current. Writing enumerators by hand is tedious; iterator methods let the compiler do it.

A method returning IEnumerable<T> that contains yield return becomes an iterator. The compiler turns it into a state machine: nothing runs when you call the method; each MoveNext() runs the body until the next yield return, hands out that value and pauses. yield break ends the sequence.

Consequences:

  • Sequences are lazy and can be infinite: Naturals().Take(5) is fine.
  • Each enumeration restarts the method from the beginning.
  • Argument validation in an iterator runs only on the first MoveNext(), not at the call. To validate eagerly, put checks in a normal wrapper method that returns the result of a private local iterator function.
  • finally blocks and using inside iterators run when enumeration ends, including when a foreach breaks early (the enumerator is disposed).

IAsyncEnumerable<T> is the async counterpart: an async method with yield return produces items consumed with await foreach. It's ideal for streaming database rows or paged API results.

This is exactly how LINQ's Where and Select are implemented, which is why they are deferred.

Example

static IEnumerable<int> Naturals()
{
    for (int n = 1; ; n++)
    {
        Console.Write($"[gen {n}] ");
        yield return n;
    }
}

foreach (var x in Naturals().Where(n => n % 2 == 0).Take(2))
    Console.Write($"{x} ");   // [gen 1] [gen 2] 2 [gen 3] [gen 4] 4
Console.WriteLine();

static async IAsyncEnumerable<string> PagesAsync()
{
    for (int page = 1; page <= 3; page++)
    {
        await Task.Delay(10);              // e.g. an HTTP call
        yield return $"page {page}";
    }
}

await foreach (var p in PagesAsync())
    Console.Write(p + "; ");               // page 1; page 2; page 3;

Remember

  • yield return turns a method into a lazy state-machine iterator
  • Nothing runs until enumeration; each enumeration restarts
  • Validate arguments eagerly in a non-iterator wrapper
  • IAsyncEnumerable + await foreach stream async data

Watch out for

  • Expecting an exception from argument checks at call time in an iterator method.

Check yourself

  1. An iterator method throws ArgumentNullException at its first line. When is it thrown for var seq = Get(null);?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/yield

Builds on

Unlocks

Nothing builds on this yet.