Iterators with yield return

Reviewed & published by Brayan K

The yield return keyword lets you build a sequence lazily — producing one element at a time, exactly when it's needed, without ever building the whole collection in memory. The compiler turns your simple loop into a sophisticated state machine behind the scenes. Master this and you'll understand how LINQ works, how to stream huge data sets, and even how to model infinite sequences safely.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a vending machine versus a catering tray. The catering tray (a List<T>) makes every sandwich up front and lays them all out, whether anyone eats them or not. The vending machine (a yield iterator) makes nothing until you press a button — then it produces exactly one item, and waits. Press again, get the next. You can walk away after two items and the machine never wasted effort on the rest. That on-demand, one-at-a-time delivery is lazy evaluation, and it's the whole point of yield return.

The mental model: pause and resume

A normal method runs top to bottom and returns once. An iterator method is different: each yield return hands back a value and pauses, remembering exactly where it stopped. The next time a value is requested, it resumes right after that line.

Because it produces values on demand, an iterator can describe an infinite sequence (every whole number, the Fibonacci series) and still be perfectly safe — the consumer simply stops pulling when it has enough.

1. Your First Iterator: yield return

An iterator method returns IEnumerable<T> but never builds a collection. Each yield return gives the caller one value and pauses. Run the worked example below and watch the "producing" lines interleave with the "got" lines — that interleaving is the proof that execution pauses and resumes rather than running all at once.

using System;
using System.Collections.Generic;

class Program
{
    // An ITERATOR method: it returns IEnumerable<int> but never builds a list.
    // Each 'yield return' hands ONE value to the caller and PAUSES here,
    // resuming from this exact spot the next time a value is requested.
    static IEnumerable<int> Countdown(int from)
    {
        for (int i = from; i >= 1; i--)
        {
            Console.WriteLine($"  (producing {i})");
            yield return i;          // give back i, then pause
        }
    }

    static void Main()
    {
        // foreach pulls values one at a time — notice 'producing' and the
        // printed value interleave, proving execution pauses and resumes.
        foreach (int n in Countdown(3))
            Console.WriteLine($"got {n}");
        // (producing 3) / got 3 / (producing 2) / got 2 / ...
    }
}

// ✅ Expected output:
//      (producing 3)
//    got 3
//      (producing 2)
//    got 2
//      (producing 1)
//    got 1

2. Lazy (Deferred) Evaluation

Calling an iterator method does not run its body — it just returns an enumerable. The body only executes when you begin enumerating, and only as far as you pull. That's why an iterator can loop forever: paired with Take(5), the consumer stops it after five items.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static IEnumerable<int> Numbers()
    {
        Console.WriteLine("  iterator body STARTS now");
        for (int i = 1; ; i++)        // infinite — but that's fine when lazy!
            yield return i;
    }

    static void Main()
    {
        // Calling Numbers() does NOT run the body yet — it's deferred.
        IEnumerable<int> seq = Numbers();
        Console.WriteLine("called Numbers(), body has not run yet");

        // Only when we enumerate (here via Take + foreach) does it execute,
        // and it stops after 5 because the consumer stops pulling.
        foreach (int n in seq.Take(5))
            Console.WriteLine(n);     // 1 2 3 4 5
    }
}

// ✅ Expected output:
//    called Numbers(), body has not run yet
//      iterator body STARTS now
//    1
//    2
//    3
//    4
//    5

Your turn. Complete the squares generator by writing the two keywords that produce each value. Fill in the ___ blanks.

using System;
using System.Collections.Generic;

class Program
{
    // 🎯 YOUR TURN — fill in the blanks marked with ___

    // Produce the squares 1, 4, 9 ... up to n*n, one at a time.
    static IEnumerable<int> Squares(int n)
    {
        for (int i = 1; i <= n; i++)
        {
            // 1) Hand back i*i as the next item in the sequence.
            ___ ___ i * i;            // 👉 the two keywords:  yield return
        }
    }

    static void Main()
    {
        foreach (int sq in Squares(3))
            Console.WriteLine(sq);

        // ✅ Expected output:
        //    1
        //    4
        //    9
    }
}

3. Ending Early with yield break

Sometimes you want to stop the sequence partway through — when you hit a sentinel value, an error, or a condition. yield break ends the iteration immediately: no further items are produced, exactly like returning from a normal method. The example stops emitting as soon as it meets a negative number.

using System;
using System.Collections.Generic;

class Program
{
    // 'yield break' ENDS the iteration early — no more items are produced.
    static IEnumerable<int> UpToFirstNegative(int[] values)
    {
        foreach (int v in values)
        {
            if (v < 0)
                yield break;          // stop the whole sequence right here
            yield return v;           // otherwise emit the value
        }
    }

    static void Main()
    {
        int[] data = { 4, 7, 2, -1, 9, 5 };
        foreach (int v in UpToFirstNegative(data))
            Console.WriteLine(v);     // 4 7 2  (stops at -1, never reaches 9, 5)
    }
}

// ✅ Expected output:
//    4
//    7
//    2

4. The Generated State Machine

You write a simple sequence; the compiler rewrites it into a hidden class that implements IEnumerator<T> and remembers where execution paused between items. foreach normally hides this, but driving the enumerator by hand with MoveNext() and Current reveals the machinery underneath.

using System;
using System.Collections.Generic;

class Program
{
    // What the compiler does FOR you: it turns this iterator into a hidden
    // state machine class that remembers WHERE it paused between items.
    // You just write the logical sequence; the machinery is generated.
    static IEnumerable<string> Steps()
    {
        yield return "step 1";   // state advances after each yield
        yield return "step 2";
        yield return "step 3";
    }

    static void Main()
    {
        // Manually driving the generated enumerator shows the state machine:
        using IEnumerator<string> e = Steps().GetEnumerator();
        while (e.MoveNext())          // MoveNext resumes the paused method
            Console.WriteLine(e.Current);  // Current holds the yielded value
        // step 1 / step 2 / step 3
    }
}

// ✅ Expected output:
//    step 1
//    step 2
//    step 3

Now you try: take a fixed count from an infinite Fibonacci iterator. Fill in the LINQ method that limits how many items you pull.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    // 🎯 YOUR TURN — fill in the blanks marked with ___

    // An INFINITE iterator producing the Fibonacci numbers, lazily.
    static IEnumerable<int> Fibonacci()
    {
        int a = 0, b = 1;
        while (true)                  // infinite is OK — the consumer stops it
        {
            yield return a;
            (a, b) = (b, a + b);
        }
    }

    static void Main()
    {
        // 1) Take only the first 6 numbers from the infinite sequence.
        foreach (int n in Fibonacci().___(6))   // 👉 the LINQ method:  Take
            Console.Write(n + " ");

        // ✅ Expected output:
        //    0 1 1 2 3 5
    }
}

🔎 Deep Dive: iterator vs returning a List<T>

Both of these methods can be consumed with foreach, but they behave very differently:

// EAGER: builds the entire list in memory before returning anything.
static List<int> AllSquaresList(int n)
{
    var list = new List<int>();
    for (int i = 1; i <= n; i++) list.Add(i * i);
    return list;                 // every item allocated up front
}

// LAZY: yields one square at a time, on demand.
static IEnumerable<int> AllSquares(int n)
{
    for (int i = 1; i <= n; i++)
        yield return i * i;      // nothing materialized until pulled
}

The List version allocates all n items immediately, even if the caller only reads the first one. The yield version produces items as needed, so it uses less memory, starts returning results sooner, and can even be infinite. The trade-off: a List can be re-read and indexed cheaply, while an iterator re-runs from the start each time you enumerate it.

Putting It Together: Streaming Log Lines with LINQ

Here's the pattern behind processing a huge file without loading it all: an iterator streams lines one at a time, and LINQ's Where and Select — themselves lazy iterators — filter and transform them on demand. Only the matching lines are ever materialized.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    // Imagine these lines streaming from a huge log file — we never want to
    // load them all at once. An iterator lets us process them one at a time.
    static IEnumerable<string> ReadLines()
    {
        yield return "INFO  app started";
        yield return "WARN  low memory";
        yield return "ERROR disk full";
        yield return "INFO  request handled";
        yield return "ERROR timeout";
    }

    static void Main()
    {
        // LINQ over the iterator stays LAZY: Where + Select each yield items
        // on demand, so only the matching lines are ever materialized.
        var errors = ReadLines()
            .Where(line => line.StartsWith("ERROR"))
            .Select(line => line.Substring(6).Trim());

        Console.WriteLine("Errors found:");
        foreach (string msg in errors)
            Console.WriteLine($"  - {msg}");
        // - disk full
        // - timeout
    }
}

// ✅ Expected output:
//    Errors found:
//      - disk full
//      - timeout

Because the whole pipeline is lazy, you could point ReadLines at a multi-gigabyte file and still keep memory flat — that's the power of composing iterators.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

ConceptCodeNotes
Produce an itemyield return x;Pauses, resumes next pull
End earlyyield break;Stops the sequence
Iterator return typeIEnumerable<int>or IEnumerator<T>
Consumeforeach (var x in seq)Drives MoveNext/Current
Limit an infinite seqseq.Take(5)Consumer stops pulling
Materializeseq.ToList()Force eager evaluation

Frequently Asked Questions

Q: When does an iterator's body actually run?

Only when you enumerate it — the first foreach iteration or MoveNext() call. Just calling the method returns the enumerable without running any of the body. This is deferred (lazy) execution.

Q: What's the difference from returning a List<T>?

A List builds every element up front; a yield iterator streams them one at a time on demand. The iterator can be infinite and uses less memory, but re-enumerates from scratch each time and can't be indexed.

Q: How can an infinite iterator not hang the program?

Because items are produced lazily, the consumer decides when to stop. Pair an infinite generator with Take(n) or First() and it produces only as many values as you ask for.

Q: Is this how LINQ works?

Yes — most LINQ operators (Where, Select, Take) are deferred iterators that yield results as you enumerate. That's why a LINQ query runs at the point you iterate it, not where you define it.

Mini-Challenge: an Even-Numbers Generator

No blanks this time — just a brief and an outline. Write an infinite Evens() iterator with yield return, then use Take(5) to print the first five even numbers. For a bonus, chain Where and Take and confirm it stays lazy. Run it and check your output against the expected lines.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: an even-numbers generator
        // 1. Write an iterator:  static IEnumerable<int> Evens()
        //    that uses 'yield return' to produce 0, 2, 4, 6, ... forever.
        // 2. In Main, use .Take(5) and a foreach to print the first five.
        // 3. BONUS: use .Where(n => n % 10 == 0).Take(3) to print the first
        //    three multiples of ten and confirm the iterator stays lazy.
        //
        // ✅ Expected (first part):
        //    0
        //    2
        //    4
        //    6
        //    8

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What does a yield return statement do inside an iterator method?

  • Produces one element and pauses the method until the next item is requested
  • Returns the whole collection at once and exits
  • Starts a new thread
  • Throws an exception

Answer: Produces one element and pauses the method until the next item is requested. yield return hands one element back to the caller and pauses execution, resuming from that exact point when the next value is requested.

What return type can a method using yield return have?

  • Only List<T>
  • void
  • IEnumerable<T>, IEnumerator<T>, or their non-generic forms
  • Task<T>

Answer: IEnumerable<T>, IEnumerator<T>, or their non-generic forms. An iterator method must return IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T>. The compiler builds a state machine that implements it.

Iterators built with yield use what kind of evaluation?

  • No evaluation at all
  • Lazy (deferred) evaluation — work happens as items are pulled
  • Eager evaluation — everything runs immediately
  • Parallel evaluation across cores

Answer: Lazy (deferred) evaluation — work happens as items are pulled. yield iterators are lazy: the method body doesn't run until you start enumerating, and it produces each item only when asked.

What does yield break do?

  • Skips the current item
  • Restarts the sequence
  • Pauses for one item
  • Ends the iteration early, like a return with no more items

Answer: Ends the iteration early, like a return with no more items. yield break stops the iterator: enumeration ends and no further items are produced, similar to returning from a normal method.

How does the C# compiler implement an iterator method under the hood?

  • By generating a hidden state machine class that implements IEnumerator
  • By copying everything into a List
  • By spawning background tasks
  • By using reflection at runtime

Answer: By generating a hidden state machine class that implements IEnumerator. The compiler rewrites the method into a generated state machine class that tracks where execution paused and resumes there on the next MoveNext call.

Why can a yield iterator represent an INFINITE sequence safely?

  • It cannot — infinite sequences are illegal
  • Because items are produced lazily and the consumer decides when to stop
  • It pre-computes all values
  • Because C# limits it to 1000 items

Answer: Because items are produced lazily and the consumer decides when to stop. Since values are produced on demand, an infinite generator like all whole numbers is fine as long as the consumer stops pulling (e.g. with Take).

Compared with building and returning a List<T>, a yield iterator typically:

  • Runs only on a background thread
  • Cannot be used with foreach
  • Always uses more memory
  • Avoids allocating the whole collection up front, streaming items one at a time

Answer: Avoids allocating the whole collection up front, streaming items one at a time. A List materializes every element immediately; a yield iterator streams items lazily, so it can save memory and start producing results sooner.

When you call an iterator method, when does its body first start running?

  • When the program exits
  • Never
  • When you begin enumerating it (e.g. the first MoveNext / foreach iteration)
  • Immediately when the method is called

Answer: When you begin enumerating it (e.g. the first MoveNext / foreach iteration). Calling the method just returns the enumerable; the body doesn't execute until enumeration begins. This deferred start is a hallmark of yield iterators.

Which interface does foreach ultimately rely on to walk a yield iterator?

  • IDisposable
  • IEnumerator<T> (obtained via IEnumerable<T>.GetEnumerator)
  • ICloneable
  • IComparable

Answer: IEnumerator<T> (obtained via IEnumerable<T>.GetEnumerator). foreach calls GetEnumerator to get an IEnumerator<T>, then repeatedly calls MoveNext and reads Current — exactly what the generated state machine provides.

How do LINQ operators like Where and Select relate to yield iterators?

  • Many are themselves lazy iterators that yield results as you enumerate
  • They always return arrays immediately
  • They disable deferred execution
  • They are unrelated to iterators

Answer: Many are themselves lazy iterators that yield results as you enumerate. Most LINQ query operators are implemented as deferred iterators; they yield items one at a time as you enumerate, which is why the query runs when you iterate it.

Continue this course

Related lessons