Enumerators #
An enumerator is a concept similar to iterators in C++. An enumerator is a read-only, forward-only cursor that allows for one-directional traversal of a sequence of values.
For an object to be called an enumerator, it must have a public parameterless bool MoveNext()
method and a Current
property. Most often, the System.Collections.Generics.IEnumerator<T>
interface (or less commonly System.Collections.IEnumerator
) is implemented, which provides the aforementioned method and property.
The definitions of these interfaces are as follows:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
Often, the Reset
method is not implemented; that is, it throws a NotSupportedException
.
Enumerable Type #
An enumerable type, in turn, is an object that is capable of creating a new enumerator. It must have a public GetEnumerator()
method that returns an enumerator (this can be an extension method). And here again, the system interface System.Collections.Generic.IEnumerable<T>
(or less commonly System.Collections.IEnumerable
) is most often implemented.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
The foreach
Loop
#
All system collections, arrays, and strings are enumerable. Anything that is enumerable can be used inside a foreach
loop:
List<int> collection = [1, 2, 3, 4, 5, 6, 7, 8];
foreach(int item in collection)
{
Console.WriteLine(item);
}
The foreach
loop is syntactic sugar; the compiler transforms this expression as follows:
IEnumerator<int> enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item);
}
}
finally
{
enumerator?.Dispose();
}
Iterator Method #
A coroutine is a special type of function that has the ability to suspend its execution at any moment and return control to the code that called it, and then, after some time, resume execution exactly from the place where it was suspended.
Iterator methods are one of two examples of coroutines in C#. Such a method must return IEnumerable<T>
or IEnumerator<T>
(or IEnumerable
or IEnumerator
) and use the yield
statement. Such a method is treated in a special way by the compiler. The compiler creates a state machine in its place, which remembers the state of the iteration, i.e., local variables and information about the suspension point.
public static IEnumerable<int> Fibonacci(int count)
{
for (int i = 0, prev = 1, curr = 1; i < count; i++)
{
yield return prev;
(prev, curr) = (curr, prev + curr);
}
}
The state machine for this method looks like this:
[CompilerGenerated]
private sealed class <Fibs>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
private int <>2__current;
private int <>l__initialThreadId;
private int count;
public int <>3__count;
private int <i>5__1;
private int <prev>5__2;
private int <curr>5__3;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
[return: Nullable(0)]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <Fibs>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
int num = <>1__state;
if (num != 0)
{
if (num != 1)
{
return false;
}
<>1__state = -1;
int num2 = <curr>5__3;
int num3 = (<curr>5__3 = <prev>5__2 + <curr>5__3);
<prev>5__2 = num2;
<i>5__1++;
}
else
{
<>1__state = -1;
<i>5__1 = 0;
<prev>5__2 = 1;
<curr>5__3 = 1;
}
if (<i>5__1 < count)
{
<>2__current = <prev>5__2;
<>1__state = 1;
return true;
}
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()
{
<Fibs>d__1 <Fibs>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<Fibs>d__ = this;
}
else
{
<Fibs>d__ = new <Fibs>d__1(0);
}
<Fibs>d__.count = <>3__count;
return <Fibs>d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<int>)this).GetEnumerator();
}
}
An iterator method is invoked after MoveNext
is called on the enumerator from its last state. The next value is calculated, the yield return
statement returns the calculated value by writing it to the Current
property, and control returns to the caller.
There is also a yield break
statement, which also returns control to the caller, but it causes MoveNext
to return false
, indicating no further elements.
public static IEnumerable<int> Fibonacci(int count)
{
for (int i = 0, prev = 1, curr = 1; i < count; i++)
{
if (i > 45) yield break;
yield return prev;
(prev, curr) = (curr, prev + curr);
}
}
Composition of Iterator Methods #
Iterator methods can be composed with each other. Let’s assume we have two methods:
public static IEnumerable<int> Fibonacci(int n)
{
int current = 0, next = 1;
for (int i = 0; i < n; i++)
{
yield return current;
(current, next) = (next, current + next);
}
}
public static IEnumerable<int> Odds(IEnumerable<int> sequence)
{
foreach (var item in sequence)
{
if (item % 2 == 1) yield return item;
}
}
They can be composed as follows:
var sequence = Odds(Fibonacci(10));
Console.WriteLine("Odd elements of the fibonacci sequence:");
foreach (var item in sequence)
{
Console.WriteLine(item);
}
The flow of control is depicted in the sequence diagram:
sequenceDiagram participant Main participant Odds participant Fibonacci activate Main Main->>+Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 1 deactivate Fibonacci activate Odds Odds->>Main: 1 deactivate Odds activate Main Main->>Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 1 deactivate Fibonacci activate Odds Odds->>Main: 1 deactivate Odds activate Main Main->>Odds: MoveNext() deactivate Main activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 2 deactivate Fibonacci activate Odds Odds->>Fibonacci: MoveNext() deactivate Odds activate Fibonacci Fibonacci->>Odds: 3 deactivate Fibonacci activate Odds Odds->>Main: 3 deactivate Odds activate Main Main->>Odds: ... deactivate Main
The Odds
method acts as a filter for the Fibonacci
sequence; most LINQ methods are based on this principle.
Source Code - IteratorMethods
Implementing a Custom Enumerable Type #
For our own custom type to be enumerable, the easiest way is to implement the IEnumerable
interface. Most often, the GetEnumerator
method is implemented as an iterator using the yield
statement.
public class Stack<T> : IEnumerable<T>
{
private T[] _items = new T[8];
public int Count { get; private set; }
public void Push(T item)
{
if (_items.Length == Count)
{
Array.Resize(ref _items, _items.Length * 2);
}
_items[Count++] = item;
}
public T Pop()
{
if (Count == 0)
{
throw new InvalidOperationException("Stack is empty");
}
return _items[--Count];
}
public IEnumerator<T> GetEnumerator()
{
int count = Count;
while (count-- > 0)
{
yield return _items[count];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Such an enumerable stack can be successfully used in a foreach
statement:
Stack<string> stack = new Stack<string>();
stack.Push("The");
stack.Push("quick");
stack.Push("brown");
stack.Push("fox");
stack.Push("jumps");
stack.Push("over");
stack.Push("the");
stack.Push("lazy");
stack.Push("dog");
foreach (var str in stack)
{
Console.WriteLine(str);
}
Source Code - EnumerableStack