SoFunction
Updated on 2025-03-06

Deeper in C# iterators and enumerators

Hello everyone, this is the 07th article in the [C#.NET Compensation for the Leaks] series.

In C#, most methods immediately return the program's control to the caller through the return statement, and also release the local resources in the method. The method containing the yield statement allows the local resource to be retained during the period when multiple values ​​are returned to the caller in sequence, and the original resource will be released when all values ​​return to the end. These returned values ​​form a sequence to be used by the caller. In C#, this method, property, or indexer that contains a yield statement is an iterator.

There are two types of yield statements in the iterator:

  • yield return, return the program control to the caller and retain the local state, and the caller gets the returned value and continues to execute it later.
  • yield break, used to tell the program that the current sequence has ended, is equivalent to a return statement that is equivalent to a normal code block (it is illegal to use return directly in the iterator).
IEnumerable<int> Fibonacci(int count)
{
 int prev = 1;
 int curr = 1;
 for (int i = 0; i < count; i++)
 {
 yield return prev;
 int temp = prev + curr;
 prev = curr;
 curr = temp;
 }
}

void Main()
{
 foreach (int term in Fibonacci(10))
 {
 (term);
 }
}

Output:

1

1

2

3

5

8

13

21

34

55

In actual scenarios, we generally rarely write iterators directly, because most scenarios that require iteration are arrays, collections, and lists, and these types have encapsulated the required iterators inside. For example, the reason why arrays in C# can be traversed is because it implements the IEnumerable interface. The Enumerator of the array can be obtained through the GetEnumerator() method, and the enumerator is implemented through an iterator. For example, the most common usage scenario is to iterate through each element in the array, such as the following example of printing array elements one by one.

int[] numbers = { 1, 2, 3, 4, 5 };
IEnumerator enumerator = ();
while (())
{
 ();
}

In fact, this is how foreach works. The above code can be rewritten using foreach as follows:

int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
 (number);
}

Of course, the enumerator does not have to be implemented through an iterator, such as the following custom enumerator, CoffeeEnumerator.

public class CoffeeCollection : IEnumerable
{
 private CoffeeEnumerator enumerator;
 public CoffeeCollection()
 {
 enumerator = new CoffeeEnumerator();
 }

 public IEnumerator GetEnumerator()
 {
 return enumerator;
 }

 public class CoffeeEnumerator : IEnumerator
 {
 string[] items = new string[3] { "espresso", "macchiato", "latte" };
 int currentIndex = -1;
 public object Current
 {
 get
 {
 return items[currentIndex];
 }
 }
 public bool MoveNext()
 {
 currentIndex++;
 if (currentIndex < )
 {
 return true;
 }
 return false;
 }
 public void Reset()
 {
 currentIndex = 0;
 }
 }
}

use:

public static void Main(string[] args)
{
 foreach (var coffee in new CoffeeCollection())
 {
 (coffee);
 }
}

Understanding iterators and enumerators can help us write more efficient code. For example, to determine whether an IEnumerable<T> object contains elements, I often see some people writing this:

if(() &gt; 0)
{
 // There are elements in the collection}

But if you think about the enumerator for a little thought, you will know that Count() must iterate all elements in order to obtain the number of set elements, and the time complexity is O(n). But just knowing whether the set contains elements, it is enough to iterate once. So a better way to be efficient is:

if(().MoveNext())
{
 // There are elements in the collection}

This way, the writing time complexity is O(1), which is obviously higher efficiency. For the convenience of writing, C# provides an extension method Any().

if(())
{
 // There are elements in the collection}

Therefore, if necessary, use the Any method as much as possible, which is more efficient.

For example, in EF Core, it needs to be executedIQueryable<T>When querying, sometimes useAsEnumerable()It is more efficient than using ToList, ToArray, etc., because ToList, ToArray, etc. will immediately perform enumeration operations, and AsEnumerable() can delay the enumeration operations until they are really needed before executing. Of course, we must also consider practical application scenarios. Array, List, etc. are more convenient for callers to use, especially to obtain the total number of elements, add and delete elements, etc.

The above is a detailed understanding of the c# iterator and enumerator. For more information about c# iterator and enumerator, please pay attention to my other related articles!