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
- Build iterators with yield return and yield break
- Understand lazy (deferred) evaluation
- Return IEnumerable<T> from an iterator method
- Represent infinite sequences safely
- See the state machine the compiler generates
- Combine iterators with LINQ and foreach
💡 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.
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 12. 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
// 5Your 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
// 24. 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 3Now 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
// - timeoutBecause 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.
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
- ✅ 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
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 — IConfiguration, appsettings binding and IOptions<T> in ASP.NET Core
- Quick reference: C# cheat sheet