Compiler-Generated State Machines for Lazy Evaluation
This article demonstrates how the C# compiler transforms methods using yield return into state machines that enable lazy evaluation. Understanding this transformation helps you write more efficient code and debug iterator-based methods.
The Simple Version
Here's a straightforward extension method that uses yield return to lazily transform elements:
public static class ExtensionMethods
{
public static IEnumerable<TResult> Map<TResult, TSource>(
this IEnumerable<TSource> items,
Func<TSource, TResult> func)
{
foreach (var item in items)
{
yield return func(item);
}
}
}
This elegant code hides significant complexity. The compiler transforms it into a sophisticated state machine.
The Compiler-Generated State Machine
Below is a human-readable version of what the compiler generates. This code won't compile as-is—it's for educational purposes only.
// REFACTORED COMPILER-GENERATED CODE
// FOR EDUCATIONAL PURPOSES ONLY
// This code will NOT compile - it's a human-readable
// version of the compiler-generated state machine
// Original method: Map<TResult, TSource>(
// IEnumerable<TSource> items,
// Func<TSource, TResult> func)
[Extension]
public static class ExtensionMethods
{
/// <summary>
/// Maps each element of a source collection to a
/// result using the provided transformation function.
/// Original code:
/// foreach (var item in items)
/// {
/// yield return func(item);
/// }
/// </summary>
[NullableContext(1)]
[IteratorStateMachine(
typeof(ExtensionMethods.MapIteratorStateMachine<,>))]
[Extension]
public static IEnumerable<TResult> Map<
[Nullable(2)] TResult,
[Nullable(2)] TSource>(
this IEnumerable<TSource> items,
Func<TSource, TResult> func)
{
// Create the state machine in its initial state
MapIteratorStateMachine<TResult, TSource> stateMachine =
new MapIteratorStateMachine<TResult, TSource>(
STATE_INITIAL_BEFORE_GET_ENUMERATOR);
// Cache the parameters for later use when
// GetEnumerator is called
stateMachine._parameterItems = items;
stateMachine._parameterFunc = func;
return (IEnumerable<TResult>)stateMachine;
}
// State machine state constants
private const int STATE_INITIAL_BEFORE_GET_ENUMERATOR = -2;
private const int STATE_RUNNING = -1;
private const int STATE_START = 0;
private const int STATE_YIELDING_VALUE = 1;
private const int STATE_ENUMERATING = -3;
[CompilerGenerated]
private sealed class MapIteratorStateMachine<
TResult, TSource> :
IEnumerable<TResult>,
IEnumerable,
IEnumerator<TResult>,
IEnumerator,
IDisposable
{
// State machine state tracking
private int _currentState;
private TResult _currentYieldedValue;
private int _initialThreadId;
// Working copies of parameters
// (used during enumeration)
private IEnumerable<TSource> _sourceItems;
private Func<TSource, TResult> _transformFunction;
// Cached parameters (from the original Map call)
public IEnumerable<TSource> _parameterItems;
public Func<TSource, TResult> _parameterFunc;
// State machine local variables
// (equivalent to locals in the original foreach)
private IEnumerator<TSource> _sourceEnumerator;
private TSource _currentSourceItem;
[DebuggerHidden]
public MapIteratorStateMachine(int initialState)
{
base..ctor();
this._currentState = initialState;
this._initialThreadId =
Environment.CurrentManagedThreadId;
}
/// <summary>
/// Dispose method - ensures the source enumerator
/// is disposed if we're in the middle of iteration
/// </summary>
[DebuggerHidden]
void IDisposable.Dispose()
{
int state = this._currentState;
// Only dispose if we're in the enumerating
// state or yielding state
if (state != STATE_ENUMERATING &&
state != STATE_YIELDING_VALUE)
return;
try
{
// Empty try block - the finally is
// what matters
}
finally
{
this.DisposeSourceEnumerator();
}
}
/// <summary>
/// The main state machine method - implements
/// the foreach loop and yield return logic
/// Original code structure:
/// foreach (var item in items)
/// {
/// yield return func(item);
/// }
/// </summary>
bool IEnumerator.MoveNext()
{
try
{
int state = this._currentState;
if (state != STATE_START)
{
if (state != STATE_YIELDING_VALUE)
return false; // Iteration complete
// Resuming after a yield return
this._currentState = STATE_ENUMERATING;
this._currentSourceItem =
default(TSource);
}
else
{
// STATE_START: Initialize the foreach
this._currentState = STATE_RUNNING;
// Get the enumerator for
// "foreach (var item in items)"
this._sourceEnumerator =
this._sourceItems.GetEnumerator();
this._currentState = STATE_ENUMERATING;
}
// Try to get the next item from the
// source collection
if (this._sourceEnumerator.MoveNext())
{
// Get the current item:
// "var item = ..."
this._currentSourceItem =
this._sourceEnumerator.Current;
// Apply the transformation:
// "func(item)"
this._currentYieldedValue =
this._transformFunction(
this._currentSourceItem);
// Yield return the transformed value
this._currentState =
STATE_YIELDING_VALUE;
return true; // We have a value
}
// No more items - clean up and finish
this.DisposeSourceEnumerator();
this._sourceEnumerator =
(IEnumerator<TSource>)null;
return false; // Iteration is complete
}
__fault // Compiler-generated fault handler
{
this.System.IDisposable.Dispose();
}
}
/// <summary>
/// Disposes the source enumerator - equivalent
/// to the implicit finally block in foreach
/// </summary>
private void DisposeSourceEnumerator()
{
this._currentState = STATE_RUNNING;
if (this._sourceEnumerator == null)
return;
this._sourceEnumerator.Dispose();
}
// IEnumerator<TResult>.Current property
// returns the current yielded value
TResult IEnumerator<TResult>.Current
{
[DebuggerHidden]
get { return this._currentYieldedValue; }
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
// IEnumerator.Current property (non-generic)
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return (object)this._currentYieldedValue;
}
}
/// <summary>
/// GetEnumerator implementation - handles
/// thread-safety and parameter initialization
/// If called on the same thread as construction
/// and in initial state, reuses this instance
/// Otherwise, creates a new state machine
/// </summary>
[DebuggerHidden]
[return: Nullable(new byte[] { 1, 0 })]
IEnumerator<TResult>
IEnumerable<TResult>.GetEnumerator()
{
MapIteratorStateMachine<TResult, TSource>
enumerator;
// Optimization: reuse this instance if we're
// on the same thread and haven't started yet
if (this._currentState ==
STATE_INITIAL_BEFORE_GET_ENUMERATOR &&
this._initialThreadId ==
Environment.CurrentManagedThreadId)
{
this._currentState = STATE_START;
enumerator = this;
}
else
{
// Create a new state machine instance
// for this enumeration
enumerator =
new MapIteratorStateMachine<
TResult, TSource>(STATE_START);
}
// Copy the cached parameters to the
// working fields
enumerator._sourceItems =
this._parameterItems;
enumerator._transformFunction =
this._parameterFunc;
return (IEnumerator<TResult>)enumerator;
}
[DebuggerHidden]
[return: Nullable(1)]
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)this
.System.Collections.Generic
.IEnumerable<TResult>.GetEnumerator();
}
}
}
Key Insights
State Management
The state machine uses several states to track execution:
-2 (Initial): Before GetEnumerator() is called
-1 (Running): Actively executing
0 (Start): Beginning of iteration
1 (Yielding): Paused at a yield return
-3 (Enumerating): Inside the foreach loop
Lazy Evaluation
The transformation doesn't happen when you call Map(). It happens when you enumerate the result. Each call to MoveNext() processes one item.
Thread Safety
The state machine includes thread-safety optimizations. If GetEnumerator() is called on the same thread that created the iterator, it reuses the instance. Otherwise, it creates a new one.
Resource Management
The state machine properly implements IDisposable to ensure the source enumerator is disposed, even if iteration stops early.
Practical Implications
Understanding this transformation helps you:
Debug iterator methods - You can see why stepping through yield return code behaves differently
Optimize performance - You understand the overhead of state machines
Design better APIs - You know when lazy evaluation is appropriate
Avoid pitfalls - You understand why captured variables behave as they do
17 November 2025