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.