How the C# yield keyword works
The C# yield keyword is an interesting keyword, because when you use it, you can return an IEnumerable<T> without specifying any concrete implementation explicitly.
Here is a code snippet:
1static IEnumerable<int> GetNumbers()
2{
3 yield return 0;
4 yield return 1;
5 yield return 2;
6}
7
8static void Main(string[] args)
9{
10 var numbers = GetNumbers();
11 foreach (var n in numbers)
12 Console.WriteLine(n);
13}
This is the output:
0
1
2
But how does this work? What does GetNumbers return exactly? Lets dig a little bit deeper:
1static void Main(string[] args)
2{
3 var numbers = GetNumbers();
4 Console.WriteLine(numbers.GetType().Name);
5}
GetType() returns the type of an object and Name gives us the name of the type.
We will see that it outputs <GetNumbers>d__0. Strange right? Because in C# you can’t use < or > in identifiers. That’s because <GetNumbers>d__0 is actually a class that’s generated by the compiler at compile time. It represents a state machine that implements GetNumbers().
To understand it better, you have to see the decompiled C#, which is quite easy with the help of SharpLab:
1namespace HelloWorld
2{
3 internal class Program
4 {
5 [CompilerGenerated]
6 private sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IDisposable, IEnumerator
7 {
8 private int <>1__state;
9
10 private int <>2__current;
11
12 private int <>l__initialThreadId;
13
14 int IEnumerator<int>.Current
15 {
16 [DebuggerHidden]
17 get
18 {
19 return <>2__current;
20 }
21 }
22
23 object IEnumerator.Current
24 {
25 [DebuggerHidden]
26 get
27 {
28 return <>2__current;
29 }
30 }
31
32 [DebuggerHidden]
33 public <GetNumbers>d__0(int <>1__state)
34 {
35 this.<>1__state = <>1__state;
36 <>l__initialThreadId = Environment.CurrentManagedThreadId;
37 }
38
39 [DebuggerHidden]
40 void IDisposable.Dispose()
41 {
42 }
43
44 private bool MoveNext()
45 {
46 switch (<>1__state)
47 {
48 default:
49 return false;
50 case 0:
51 <>1__state = -1;
52 <>2__current = 0;
53 <>1__state = 1;
54 return true;
55 case 1:
56 <>1__state = -1;
57 <>2__current = 1;
58 <>1__state = 2;
59 return true;
60 case 2:
61 <>1__state = -1;
62 <>2__current = 2;
63 <>1__state = 3;
64 return true;
65 case 3:
66 <>1__state = -1;
67 return false;
68 }
69 }
70
71 bool IEnumerator.MoveNext()
72 {
73 //ILSpy generated this explicit interface implementation from .override directive in MoveNext
74 return this.MoveNext();
75 }
76
77 [DebuggerHidden]
78 void IEnumerator.Reset()
79 {
80 throw new NotSupportedException();
81 }
82
83 [DebuggerHidden]
84 IEnumerator<int> IEnumerable<int>.GetEnumerator()
85 {
86 if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
87 {
88 <>1__state = 0;
89 return this;
90 }
91 return new <GetNumbers>d__0(0);
92 }
93
94 [DebuggerHidden]
95 IEnumerator IEnumerable.GetEnumerator()
96 {
97 return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
98 }
99 }
100
101 [IteratorStateMachine(typeof(<GetNumbers>d__0))]
102 private static IEnumerable<int> GetNumbers()
103 {
104 return new <GetNumbers>d__0(-2);
105 }
106
107 private static void Main(string[] args)
108 {
109 Console.WriteLine(GetNumbers().GetType().Name);
110 }
111 }
112}
As you can see, there is a hidden <GetNumbers>d__0 class generated by the compiler and GetNumbers() is modified to return a new instance of that class. The most interesting part of <GetNumbers>d__0 is the MoveNext() method in which the compiler translates the logic in GetNumbers() into a state machine.
Roslyn (the C# compiler) generates a lot of code on your behalf, this operation is called lowering. Some other examples are when you use a for each or when you use async/await.
Knowing how the compiler translates your code helps you to understand the code better and it also helps you in troubleshooting. For example, if we have a piece of code like this:
1static IEnumerable<int> GetNumbers()
2{
3 yield return 0;
4 throw new Exception();
5}
6
7static void Main(string[] args)
8{
9 try
10 {
11 var numbers = GetNumbers();
12 foreach(var n in numbers)
13 {
14 Console.WriteLine(n);
15 }
16 }
17 catch (Exception ex)
18 {
19 Console.WriteLine(ex);
20 }
21}
This will be the output:
0
System.Exception: Exception of type 'System.Exception' was thrown.
at HelloWorld.Program.<GetNumbers>d__0.MoveNext()
at HelloWorld.Program.Main(String[] args)
Notice how the first element is returned and then an exception is thrown in <GetNumbers>d__0.MoveNext(). Knowing what is <GetNumbers>d__0 makes you more comforatable dealing with these kinds of exceptions.