Code Quality Design Help

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:

  1. Debug iterator methods - You can see why stepping through yield return code behaves differently

  2. Optimize performance - You understand the overhead of state machines

  3. Design better APIs - You know when lazy evaluation is appropriate

  4. Avoid pitfalls - You understand why captured variables behave as they do

17 November 2025