After reading the great article about the code-saving yield keyword “Give way to the yield keyword” by Shay Friedman I thought it could be interesting to know how the yield keyword works behind the scenes.
…it doesn’t really end the method’s execution. yield return pauses the method execution and the next time you call it (for the next enumeration value), the method will continue to execute from the last yield return call. It sounds a bit confusing I think… (ShayF)
By using yield return within a method that returns IEnumerable or IEnumeratorthe language feature is activated.
Note: IEnumerable is kind of a stateless factory for Enumerators.IEnumerable.GetEnumerator() is thread safe and can be called multiple times, while the returned stateful Enumerator is just a helper for enumerating contained values once. By contract IEnumerator offers a Reset() method, but many implementations just throw a NotSupportedException.
Lets create an enumerator method that yields some Fibonacci nubmers.
public class YieldingClass { public IEnumerable<int> GetFibonachiSequence() { yield return 1; yield return 2; yield return 3; yield return 5; } }
Note: Yield is not a feature of the .Net runtime. It is just a C# language feature which gets compiled into simple IL code by the C# compiler.
The compiler now generates a inner class with following signature (Reflector + some renaming):
[CompilerGenerated] private sealed class YieldingEnumerator : IEnumerable<object>, IEnumerator<object> { // Fields private int state; private int current; public YieldingClass owner; private int initialThreadId; // Methods [DebuggerHidden] public YieldingEnumerator(int state); private bool MoveNext(); [DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator(); [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator(); [DebuggerHidden] void IEnumerator.Reset(); void IDisposable.Dispose(); // Properties object IEnumerator<object>.Current { [DebuggerHidden] get; } object IEnumerator.Current { [DebuggerHidden] get; } }
Each enumerator holds a state indicating:
- -2: Initialized as Enumerable. (Not yet an Enumerator)
- -1: Closed
- 0: Initialized as Enumerator.
If a new Enumerator is requested on the same instance, GetEnumerator() returns another new instance of YieldingEnumerator. - 1-n: Index of the yield return in the original GetFibonachiSequence()method. In case of nested enumerators or other more complex scenarios one yield return consumes more than one index.
The content of GetFibonachiSequence() is translated into YieldingEnumerator.MoveNext().
In our very simple scenario the code looks like this:
bool MoveNext() { switch (state) { case 0: state = -1; current = 1; state = 1; return true; case 1: state = -1; current = 2; state = 2; return true; case 2: state = -1; current = 3; state = 3; return true; case 3: state = -1; current = 5; state = 4; return true; case 4: state = -1; break; } return false; }
So far we easily could have created the classes and methods used to enable the yield keyword ourselves, too.
But in more complex scenarios Microsoft does some tricks, which won’t compile as C# – at least not how Reflector translates the resulting IL code.
Lets have a look at some code with a nested enumeration…
foreach(int i in new int[] {1, 2, 3, 5, 8}) { yield return i; }
private bool MoveNext() { try { switch (state) { case 0: state = -1; state = 1; this.values = new int[] { 1, 2, 3, 5, 8 }; this.currentPositionInValues = 0; while (this.currentPositionInValues < this.values.Length) { current_i = this.values[this.currentPositionInValues]; current = current_i; state = 2; return true; Label_007F: state = 1; this.currentPositionInValues++; } this.Finally2(); break; case 2: goto Label_007F; } return false; } fault { this.System.IDisposable.Dispose(); } } [/sourcecode]
Now the states 1 and 2 are used to indicate whether the enumerator actually is at some point (2), or wether it is trying to retrieve the next value (1).
Two things would not compile:
- goto Label_007F is used to jump back into the iteration over int[] values. The C# goto statement is not able to jump into another statements context. But in IL this is totally valid as a while statement in MSIL is nothing but some gotos either.
- The fault is proper MSIL, but not supported in C#. Basically it acts as a finally which just is executed in case of an error.
Attention: As in anonymous delegates, parameters as well as local and instance variables are passed to the YieldingEnumerator only once. Read this great post on this: Variable Scoping in Anonymous Delegates in C#
Thanks for your attention!