yield: The Compiler Writes Your State Machine
What yield return compiles into, yield break, and the observable facts of deferred execution.
An iterator method is rewritten, not called
Mark a method IEnumerable<T> with yield return and the compiler generates
a state-machine class: each yield return is a pause point, each MoveNext a
resume:
static IEnumerable<int> Evens(int upto)
{
for (int i = 0; i <= upto; i += 2)
yield return i; // pause: hand one value out, wait
}
Nothing runs when Evens(10) is CALLED โ the method body starts only when
the first MoveNext happens (the first foreach step). Side effects before
the first yield are equally deferred:
static IEnumerable<int> Logged()
{
Console.WriteLine("start"); // deferred too!
yield return 1;
Console.WriteLine("middle");
yield return 2;
}
yield break terminates early (an empty iterator is just yield break).
Inside iterator methods: no yield return inside try-catch (only try-
finally), no ref/out parameters, single yield path per resume.
Deferred โ cached
Each foreach over an iterator RE-RUNS it:
var seq = Logged();
foreach (var x in seq) { } // prints start, middle
foreach (var x in seq) { } // prints start, middle AGAIN
Materialize with ToList() when you need one-shot semantics or multiple
passes over expensive work โ and expect the tradeoff (memory for time).
Check your understanding
- When does the code before the first
yieldexecute? (On first MoveNext, not at call.) - Why can't iterator methods take
refparameters? (State machines are heap objects; ref locals can't persist across suspensions.)