DEV Community

loading...

How the C# yield keyword works

mhmd_azeez profile image Muhammad Azeez Originally published at mazeez.dev Updated on ・3 min read

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:

static IEnumerable<int> GetNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
}

static void Main(string[] args)
{
    var numbers = GetNumbers();
    foreach (var n in numbers)
        Console.WriteLine(n);
}

This is the output:

0
1
2

But how does this work? What does GetNumbers return exactly? Lets dig a little bit deeper:

static void Main(string[] args)
{
    var numbers = GetNumbers();
    Console.WriteLine(numbers.GetType().Name);
}

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:

namespace HelloWorld
{
    internal class Program
    {
        [CompilerGenerated]
        private sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IDisposable, IEnumerator
        {
            private int <>1__state;

            private int <>2__current;

            private int <>l__initialThreadId;

            int IEnumerator<int>.Current
            {
                [DebuggerHidden]
                get
                {
                    return <>2__current;
                }
            }

            object IEnumerator.Current
            {
                [DebuggerHidden]
                get
                {
                    return <>2__current;
                }
            }

            [DebuggerHidden]
            public <GetNumbers>d__0(int <>1__state)
            {
                this.<>1__state = <>1__state;
                <>l__initialThreadId = Environment.CurrentManagedThreadId;
            }

            [DebuggerHidden]
            void IDisposable.Dispose()
            {
            }

            private bool MoveNext()
            {
                switch (<>1__state)
                {
                    default:
                        return false;
                    case 0:
                        <>1__state = -1;
                        <>2__current = 0;
                        <>1__state = 1;
                        return true;
                    case 1:
                        <>1__state = -1;
                        <>2__current = 1;
                        <>1__state = 2;
                        return true;
                    case 2:
                        <>1__state = -1;
                        <>2__current = 2;
                        <>1__state = 3;
                        return true;
                    case 3:
                        <>1__state = -1;
                        return false;
                }
            }

            bool IEnumerator.MoveNext()
            {
                //ILSpy generated this explicit interface implementation from .override directive in MoveNext
                return this.MoveNext();
            }

            [DebuggerHidden]
            void IEnumerator.Reset()
            {
                throw new NotSupportedException();
            }

            [DebuggerHidden]
            IEnumerator<int> IEnumerable<int>.GetEnumerator()
            {
                if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
                {
                    <>1__state = 0;
                    return this;
                }
                return new <GetNumbers>d__0(0);
            }

            [DebuggerHidden]
            IEnumerator IEnumerable.GetEnumerator()
            {
                return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
            }
        }

        [IteratorStateMachine(typeof(<GetNumbers>d__0))]
        private static IEnumerable<int> GetNumbers()
        {
            return new <GetNumbers>d__0(-2);
        }

        private static void Main(string[] args)
        {
            Console.WriteLine(GetNumbers().GetType().Name);
        }
    }
}

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:

static IEnumerable<int> GetNumbers()
{
    yield return 0;
    throw new Exception();
}

static void Main(string[] args)
{
    try
    {
        var numbers = GetNumbers();
        foreach(var n in numbers)
        {
            Console.WriteLine(n);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

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.

Discussion (2)

pic
Editor guide
Collapse
saint4eva profile image
saint4eva

Nice article. Thank you.

Collapse
mhmd_azeez profile image