Iterators with yield return
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.
Learn Iterators with yield return in our free C# course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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.
- ⏸️ yield return x — produce one value, then pause until the next item is pulled.
- 🛑 yield break — end the sequence early, like a return with no more items.
- 🦥 lazy / deferred — the body doesn't run until you start enumerating, and only as far as you pull.
- ⚙️ state machine — the compiler generates a hidden class that tracks where execution paused.
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.
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.
Your turn. Complete the squares generator by writing the two keywords that produce each value. Fill in the ___ blanks.
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.
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.
Now you try: take a fixed count from an infinite Fibonacci iterator. Fill in the LINQ method that limits how many items you pull.
🔎 Deep Dive: iterator vs returning a List < T >
Both of these methods can be consumed with foreach , but they behave very differently:
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.
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
- 💡 Remember it's deferred: code in an iterator runs when enumerated, not when called — so argument validation may need a wrapper to throw eagerly.
- 💡 Enumerating twice re-runs the iterator from the start; call .ToList() if you need to read the results more than once.
- 💡 Use iterators for infinite or huge sequences where building a full collection would be wasteful or impossible.
- 💡 Pair with Take / First to consume just enough of an infinite or expensive sequence.
- 💡 For async streams , the same idea becomes IAsyncEnumerable < T > with await foreach — covered in the channels lesson.
Common Errors (and the fix)
- "CS1624: The body cannot be an iterator because its return type is not an iterator interface type": change the return type to IEnumerable < T > or IEnumerator < T > .
- Argument validation doesn't throw: because the body is deferred, a guard like if (n < 0) throw won't run until enumeration. Split into a non-iterator wrapper that validates, then calls a private iterator.
- "CS1626: Cannot yield a value in the body of a try block with a catch clause": you can't yield inside a try that has a catch . Restructure so the yield is outside it.
- Expecting the work to happen at call time: calling the method only returns the enumerable; nothing runs until you foreach or call MoveNext .
- Enumerating an expensive iterator repeatedly: each foreach re-executes the whole sequence. Materialize once with .ToList() if you'll reuse it.
📋 Quick Reference
Concept
Code
Notes
Produce an item
yield return x;
Pauses, resumes next pull
End early
yield break;
Stops the sequence
Iterator return type
IEnumerable<int>
or IEnumerator<T>
Consume
foreach (var x in seq)
Drives MoveNext/Current
Limit an infinite seq
seq.Take(5)
Consumer stops pulling
Materialize
seq.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.
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.
🎉 Lesson Complete
- ✅ yield return produces one value and pauses; yield break ends the sequence
- ✅ Iterator methods return IEnumerable < T > / IEnumerator < T >
- ✅ Execution is lazy (deferred) — the body runs only as you enumerate
- ✅ The compiler generates a hidden state machine that tracks where it paused
- ✅ Lazy evaluation makes infinite sequences safe when paired with Take
- ✅ Iterators power foreach and most LINQ operators
- ✅ Next lesson: Configuration & the Options Pattern in ASP.NET Core
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
- Previous: Nullable Reference Types
- Next: Configuration & the Options Pattern