I was toying around with yield
and IEnumerable
and I'm now curious why or how the following snippet works:
public class FakeList : IEnumerable<int> { private int one; private int two; public IEnumerator<int> GetEnumerator() { yield return one; yield return two; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
Now how does the compiler turn this:
public IEnumerator<int> GetEnumerator() { yield return one; yield return two; }
into an IEnumerator<int>
?
When a yield return statement is reached in the iterator method, expression is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.
It specifies that an iterator has come to an end. You can think of yield break as a return statement which does not return a value. For example, if you define a function as an iterator, the body of the function may look like this: for (int i = 0; i < 5; i++) { yield return i; } Console.
IEnumerable has just one method called GetEnumerator. This method returns another type which is an interface that interface is IEnumerator. If we want to implement enumerator logic in any collection class, it needs to implement IEnumerable interface (either generic or non-generic).
"yield break" breaks the Coroutine (it's similar as "return"). "yield return null" means that Unity will wait the next frame to finish the current scope. "yield return new" is similar to "yield return null" but this is used to call another coroutine.
When using yield return
, the compiler generates an enumerator class for you. So the actual code which is used is much more complex than just two return statements. The compiler adds all necessary code to return an enumerator for you, which iterates over the results from the yield return
.
This is the generated code from your FakeList.GetEnumerator()
:
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; public class FakeList : IEnumerable<int>, IEnumerable { private int one; private int two; [IteratorStateMachine(typeof(<GetEnumerator>d__2))] public IEnumerator<int> GetEnumerator() { yield return this.one; yield return this.two; } IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); [CompilerGenerated] private sealed class <GetEnumerator>d__2 : IEnumerator<int>, IDisposable, IEnumerator { private int <>1__state; private int <>2__current; public FakeList <>4__this; [DebuggerHidden] public <GetEnumerator>d__2(int <>1__state) { this.<>1__state = <>1__state; } private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<>2__current = this.<>4__this.one; this.<>1__state = 1; return true; case 1: this.<>1__state = -1; this.<>2__current = this.<>4__this.two; this.<>1__state = 2; return true; case 2: this.<>1__state = -1; return false; } return false; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] void IDisposable.Dispose() { } int IEnumerator<int>.Current => this.<>2__current; object IEnumerator.Current => this.<>2__current; } }
Do you see the <GetEnumerator>d__2
class? That is generated based on your two yield return
s.
When the compiler sees yield return
or yield break
it takes the function and coverts the logic it into a class that implements a state machine. An instance of this class is then returned when the method is called.
C# In Depth has a section on what the code looks like.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With