Muhammad Azeez

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.

#csharp #roslyn