1. What Does yield Do?
yield return
The code below demonstrates how yield return enables iteration over a sequence, similar to using foreach on an array. In essence, yield return is used to create an iterator.
using static System.Console;
using System.Collections.Generic;
class Program
{
// A method returning IEnumerable<int>, containing three yield return statements
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
static void Main(string[] args)
{
// Iterate over the sequence using foreach
foreach (int item in GetNumbers())
{
WriteLine(item);
}
ReadKey();
}
}
// Output:
// 1
// 2
// 3
yield break
In the following example, only 1 and 2 are printed; 3 is not. This is because yield break terminates the iterator early.
using static System.Console;
using System.Collections.Generic;
class Program
{
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield break;
yield return 3; // This line is never reached
}
static void Main(string[] args)
{
foreach (int item in GetNumbers())
{
WriteLine(item);
}
ReadKey();
}
}
// Output:
// 1
// 2
2. Where Can yield Be Used?
The yield keyword can only appear in methods, operators, or get accessors that have a return type of IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T>.
3. How Does yield Work Under the Hood?
Let's replace foreach with a while loop. Even though we did not manual implement GetEnumerator(), MoveNext(), or the Current property, the code still works.
class Program
{
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
static void Main(string[] args)
{
// Using while loop instead of foreach
IEnumerator<int> enumerator = GetNumbers().GetEnumerator();
while (enumerator.MoveNext())
{
int current = enumerator.Current;
WriteLine(current);
}
ReadKey();
}
}
// Output:
// 1
// 2
// 3
To understand why this works, we can decompile the compiled assembly (e.g., using ILSpy).
When decompiling to C#, the code looks unchanged. How ever, if we decompile to IL with C# comments, we see that the compiler automatically generates a new class.

The GetNumbers() method returns an instance of this generated class.

This generated class implements IEnumerable, IEnumerable<T>, IEnumerator, and IEnumerator<T>. That is why we can use MoveNext() and Current without implementing them manually.

Let's look at how the MoveNext() method is implemented in this generated class.


Each call to MoveNext() increments a state field. The method uses a switch statement (based on state) to execute different parts of the original method. For the example with three yield return statements, MoveNext() returns true three times (states 1, 2, 3) and then returns false (state 4), ending the iteration.
Actual Flow of Iteration Using yield
- The method (e.g.,
GetNumbers()) is called, returning an instance of the compiler-generated class. GetEnumerator()is called on that instance, returning the iterator (the same instance often implements bothIEnumerableandIEnumerator).- Each call to
MoveNext()incrementsstateand uses aswitchto execute the correct code segment, untilMoveNext()returnsfalse.
In short, the yield keyword is syntactic sugar. The compiler trensforms the method into a state machine that implements the IEnumerable<T>, IEnumerable, IEnumerator<T>, and IEnumerator interfaces, thus providing the iteration functionality.