Enumerators and Yield in C#

C# defines two interfaces to support enumeration. The namespace System.Collections contains these declarations,

IEnumerator and IEnumerable declarations

  1. public interface IEnumerator  
  2. {  
  3.     bool MoveNext();  
  4.     object Current  
  5.     {  
  6.         get;  
  7.     }  
  8.     void Reset();  
  9. }  
  10. public interface IEnumerable  
  11. {  
  12.     IEnumerator GetEnumerator();  
  13. }  
An object that implements IEnumerable can be enumerated through an object that implements IEnumerator. The enumeration can be performed by calling the MoveNext method until it returns false.

The code below defines a class that can be enumerated in this way. As you can see, the CountdownEnumerator class is more complex, and it implements the enumeration logic in a single place. In this sample, the enumerator does not really enumerate anything but simply returns descending numbers starting from the StartCountdown number defined in the Countdown class (which is also the enumerated class).

Enumerable class
  1. public class Countdown: IEnumerable  
  2. {  
  3.     public int StartCountdown;  
  4.     public IEnumerator GetEnumerator()  
  5.     {  
  6.         return new CountdownEnumerator(this);  
  7.     }  
  8. }  
  9. public class CountdownEnumerator: IEnumerator  
  10. {  
  11.     private int _counter;  
  12.     private Countdown _countdown;  
  13.     public CountdownEnumerator(Countdown countdown)  
  14.     {  
  15.         _countdown = countdown;  
  16.         Reset();  
  17.     }  
  18.     public bool MoveNext()  
  19.     {  
  20.         if (_counter > 0)  
  21.         {  
  22.             _counter--;  
  23.             return true;  
  24.         }  
  25.         else  
  26.         {  
  27.             return false;  
  28.         }  
  29.     }  
  30.     public void Reset()  
  31.     {  
  32.         _counter = _countdown.StartCountdown;  
  33.     }  
  34.     public object Current  
  35.     {  
  36.         get  
  37.         {  
  38.             return _counter;  
  39.         }  
  40.     }  
  41. }  
The real enumeration happens only when the CountdownEnumerator is used by a code block.

Sample enumeration code
  1. public class DemoEnumerator  
  2. {  
  3.     public static void DemoCountdown()  
  4.     {  
  5.         Countdown countdown = new Countdown();  
  6.         countdown.StartCountdown = 5;  
  7.         IEnumerator i = countdown.GetEnumerator();  
  8.         while (i.MoveNext())  
  9.         {  
  10.             int n = (int) i.Current;  
  11.             Console.WriteLine(n);  
  12.         }  
  13.         i.Reset();  
  14.         while (i.MoveNext())  
  15.         {  
  16.             int n = (int) i.Current;  
  17.             Console.WriteLine("{0} BIS", n);  
  18.         }  
  19.     }  
  20.     // …  
  21. }  
The GetEnumerator call provides the enumerator object. We make two loops on it just to show the use of the Reset method. We need to cast the Current return value to int because we are using the nongeneric version of the enumerator interfaces.

Note: C# 2.0 introduced enumeration support through generics. The namespace System.Collections.Generic contains generic IEnumerable<T> and IEnumerator<T> declarations.

These interfaces eliminate the need to convert data in and out from an object type. This capability is important when enumerating value types because there are no more box or unbox operations that might affect performance.

Enumeration using a foreach statement
  1. public class DemoEnumeration  
  2. {  
  3.     public static void DemoCountdownForeach()  
  4.     {  
  5.         Countdown countdown = new Countdown();  
  6.         countdown.StartCountdown = 5;  
  7.         foreach(int n in countdown)  
  8.         {  
  9.             Console.WriteLine(n);  
  10.         }  
  11.         foreach(int n in countdown)  
  12.         {  
  13.             Console.WriteLine("{0} BIS", n);  
  14.         }  
  15.     }  
  16.     // …  
  17. }  
Using foreach, the compiler generates an initial call to GetEnumerator and a call to MoveNext before each loop. The real difference is that the code generated by foreach never calls the Reset method: two instances of CountdownEnumerator objects are created instead of one.

Note: The foreach statement can also be used with classes that do not expose an IEnumerable interface but that have a public GetEnumerator method.

C# 2.0 introduced the yield statement through which the compiler automatically generates a class that implements the IEnumerator interface returned by the GetEnumerator method. The yield statement can be used only immediately before a return or break keyword. The code in below code generates a class equivalent to the previous CountdownEnumerator.

Enumeration using a yield statement
  1. public class CountdownYield: IEnumerable  
  2. {  
  3.     public int StartCountdown;  
  4.     public IEnumerator GetEnumerator()  
  5.     {  
  6.         for (int i = StartCountdown - 1; i >= 0; i--)  
  7.         {  
  8.             yield  
  9.             return i;  
  10.         }  
  11.     }  
  12. }  
From a logical point of view, the yield return statement is equivalent to suspending execution, which is resumed at the next MoveNext call. Remember that the GetEnumerator method is called only once for the whole enumeration, and it returns a class that implements an IEnumerator interface. Only that class really implements the behavior defined in the method that contains the yield statement.

A method that contains yield statements is called an iterator. An iterator can include many yield statements. The code in below is perfectly valid and is functionally equivalent to the previous CountdownYield class with a StartCountdown value of 5.

Multiple yield statements
  1. public class CountdownYieldMultiple: IEnumerable  
  2. {  
  3.     public IEnumerator GetEnumerator()  
  4.     {  
  5.         yield  
  6.         return 4;  
  7.         yield  
  8.         return 3;  
  9.         yield  
  10.         return 2;  
  11.         yield  
  12.         return 1;  
  13.         yield  
  14.         return 0;  
  15.     }  
  16. }  
By using the generic version of IEnumerator, it is possible to define a strongly typed version of the CountdownYield class, shown in below code.

Enumeration using yield (typed)
  1. public class CountdownYieldTypeSafe: IEnumerable < int >  
  2. {  
  3.     public int StartCountdown;  
  4.     IEnumerator IEnumerable.GetEnumerator()  
  5.     {  
  6.         return this.GetEnumerator();  
  7.     }  
  8.     public IEnumerator < int > GetEnumerator()  
  9.     {  
  10.         for (int i = StartCountdown - 1; i >= 0; i--)  
  11.         {  
  12.             yield  
  13.             return i;  
  14.         }  
  15.     }  
  16. }  
The strongly typed version contains two GetEnumerator methods: one is for compatibility with nongeneric code (returning IEnumerable), and the other is the strongly typed one (returning IEnumerator<int>).

The internal implementation of LINQ to Objects makes extensive use of enumerations and yield. Even if they work under the covers, keep their behavior in mind while you are debugging code.

 

Ebook Download
View all
Learn
View all