An enumerator is a read-only, forward-only cursor over a sequence of values. An enumerator is an object that either:
The foreach
statement iterates over an
enumerable object. An enumerable object is the logical
representation of a sequence, and is not itself a cursor, but an object that produces
cursors over itself. An enumerable object either:
IEnumerator
and IEnumerable
are defined in System. Collections.
IEnumerator<T>
and IEnumerable<T>
are defined in System.Collections.Generic
.
The enumeration pattern is as follows:
classEnumerator
// typically implements IEnumerator // or IEnumerator<T> { publicIteratorVariableType
Current { get {...} } public bool MoveNext( ) {...} } classEnumerable
// typically implements IEnumerable // or IEnumerable<T> { publicEnumerator
GetEnumerator( ) {...} }
Here is the high-level way of iterating through the characters in the word “beer”
using a foreach
statement:
foreach (char c in "beer") Console.WriteLine (c);
Here is the low-level way of iterating through the characters in the word “beer”
without using a foreach
statement:
var enumerator = "beer".GetEnumerator();
while (enumerator.MoveNext())
{ var element = enumerator.Current;
Console.WriteLine (element); }
The foreach
statement also acts as a using
statement, implicitly disposing the enumerator
object.
Whereas a foreach
statement is a
consumer of an enumerator, an iterator is a
producer of an enumerator. In this example, we use an iterator to
return a sequence of Fibonacci numbers (where each number is the sum of the previous
two):
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in Fibs(6))
Console.Write (fib + " ");
}
static IEnumerable<int> Fibs(int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1;
i < fibCount;
i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
}
OUTPUT: 1 1 2 3 5 8
Whereas a return
statement expresses “Here’s the
value you asked me to return from this method,” a yield
return
statement expresses “Here’s the next element you asked me to yield from
this enumerator.” On each yield
statement, control is
returned to the caller, but the callee’s state is maintained so that the method can
continue executing as soon as the caller enumerates the next element. The lifetime of this
state is bound to the enumerator, such that the state can be released when the caller has
finished enumerating.
The compiler converts iterator methods into private classes that implement IEnumerable<T>
and IEnumerator<T>
. The logic within the iterator block is “inverted” and
spliced into the MoveNext
method and Current
property on the compiler-written enumerator class.
This means that when you call an iterator method, all you’re doing is instantiating the
compiler-written class; none of your code actually runs! Your code runs only when you
start enumerating over the resultant sequence, typically with a foreach
statement.
An iterator is a method, property, or indexer that contains one or more yield
statements. An iterator must return one of the following
four interfaces (otherwise, the compiler will generate an error):
// Enumerable interfaces System.Collections.IEnumerable System.Collections.Generic.IEnumerable<T> // Enumerator interfaces System.Collections.IEnumerator System.Collections.Generic.IEnumerator<T>
Iterators that return an
enumerator interface tend to be used less often. They’re useful
when writing a custom collection class: typically, you name the iterator GetEnumerator
and have your class implement IEnumerable<T>
.
Iterators that return an enumerable interface are more common—and
simpler to use because you don’t have to write a collection class. The compiler, behind
the scenes, writes a private class implementing IEnumerable<T>
(as well as IEnumerator<T>
).
In iterator can include multiple yield
statements. For example:
static void Main() { foreach (string s in Foo()) Console.Write (s + " "); // One Two Three } static IEnumerable<string> Foo() {yield return
"One";yield return
"Two";yield return
"Three"; }
The yield break
statement indicates that the
iterator block should exit early, without returning more elements. We can modify the
preceding Foo
method to demonstrate:
static IEnumerable<string> Foo(bool breakEarly)
{
yield return "One";
yield return "Two";
if (breakEarly)
yield break;
yield return "Three";
}
Iterators are highly composable. We can extend our example, this time to output only even Fibonacci numbers:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
foreach (int fib in EvenNumbersOnly (Fibs (6)))
Console.WriteLine(fib);
}
static IEnumerable<int> Fibs (int fibCount)
{
for (int i = 0, prevFib = 1, curFib = 1;
i < fibCount;
i++)
{
yield return prevFib;
int newFib = prevFib+curFib;
prevFib = curFib;
curFib = newFib;
}
}
static IEnumerable<int> EvenNumbersOnly (
IEnumerable<int> sequence)
{
foreach(int x in sequence)
if ((x % 2) == 0)
yield return x;
}
}
Each element is not calculated until the last moment—when requested by a MoveNext()
operation. Figure 1-5 shows the data requests and data output over
time.
The composability of the iterator pattern is extremely useful in building LINQ queries.