Understand iterator methods and the yield keyword in C# with the help of examples
An iterator in C# is a method that utilizes the yield
keyword to return elements one at a time to the caller. Such methods are very useful for traversing collections, without needing to create an intermediate collection inside the method for storing results.
This article will help you understand how to use yield
by using practical examples.
The yield
keyword can be used inside a method in two forms:
-
yield return <expression>;
which applies an expression to the current element of a collection and returns the result. After ayield return
is called, the iterator method maintains its state and the next time the method is called, execution resumes from this location. -
yield break;
which ends the iteration.
The yield return example
We assume an array with integers. We iterate over the elements and for each element add the current element to the sum previous elements to get the running sum.
We do not want to create an intermediate collection in our method for storing the running sums. Instead we use yield return
to get the sum for each element and return it to the caller. The GetRunningSum
method is now an iterator method, works as a state machine, since it remembers on which element it stopped, and proceeds to the next element when the caller allows it.
var integers = new int[] {1, 5, 12, 1, 2};
YieldTest yieldTest = new();
foreach (int i in yieldTest.GetRunningSum(integers))
{
Console.WriteLine(i);
}
public class YieldTest
{
public IEnumerable<int> GetRunningSum(int[] numbers)
{
var sum = 0;
foreach (int number in numbers)
{
sum += number;
yield return sum;
}
}
}
If you run this code in a console application, you will get:
1
6
18
19
21
The iterator-method waiting for an external action example
We now focus on a more complex example, where the same iterator method GetRunningSum
, retains state and waits for the caller to resume the execution with the next element in the collection.
This time the caller is going to run extra code with each returned sum before letting the iterator method to proceed to the next element of the collection.
var integers = new int[] {1, 5, 12, 1, 2};
YieldTest yieldTest = new();
IEnumerator<int> enumerator = yieldTest.GetRunningSum(integers).GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine("Current sum: " + enumerator.Current);
Console.WriteLine("Press Enter to continue...");
Console.ReadLine(); // Wait for user input
}
These are the main takeaways from this code:
- The
GetEnumerator
method returns an enumerator. However, the actual execution ofGetRunningSum
method begins when you start iterating over the collection by calling theMoveNext
method in the while-loop. - The
MoveNext
method in the while-loop resumes the execution of the iterator-method from the element it was left off. - Pay attention to the
Current
property of the IEnumerator. It points to the current element of the collection. - The code resumes after the user presses a button. A example from production code could be that the caller does a DB call with the sum returned
yield
.
The yield break example
The yield break;
statement is used to end the iteration early based on a condition. In the following example we are updating the GetRunningSum
with a condition for a sum exceeding a certain threshold:
public IEnumerable<int> GetRunningSum(int[] numbers)
{
var sum = 0;
foreach (int i in numbers)
{
sum += i;
if (sum > 20)
{
yield break;
}
yield return sum;
}
}
The async yield example
It is possible to call asynchronous methods in the yield return <expression>;
. For that you will have to use the IAsyncEnumerable<T>
as a return type of the iterator method. This feature is supported starting from C# 8. Here is an example of an async call:
public async IAsyncEnumerable<int> GetRunningSum(int[] numbers)
{
var sum = 0;
foreach (int i in numbers)
{
sum += i;
if (sum > 20)
{
yield break;
}
yield return await AnotherMethodAsync(sum);
}
}
Conclusion
The yield return
simplifies the creation of iterators, allowing methods to return elements one at a time while maintaining their state between calls. This feature is particularly useful for processing large datasets or streams of data efficiently.
I hope my examples clarified how to use the keyword in your code.