Enumeration and Iterators

Enumeration

An enumerator is a read-only, forward-only cursor over a sequence of values. An enumerator is an object that either:

  • Implements IEnumerator or IEnumerator<T>

  • Has a method named MoveNext for iterating the sequence, and a property called Current for getting the current element in the sequence

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:

  • Implements IEnumerable or IEnumerable<T>

  • Has a method named GetEnumerator that returns an enumerator

Note

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:

	class Enumerator   // typically implements IEnumerator
	                   // or IEnumerator<T>
	{
	  public IteratorVariableType Current { get {...} }
	  public bool MoveNext( )             {...}
	}

	class Enumerable   // typically implements IEnumerable
	                   // or IEnumerable<T>
	{
	  public Enumerator 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.

Iterators

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.

Warning

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.

Iterator Semantics

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>).

Multiple yield statements

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";
	}

yield break

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";
	}

Warning

A return statement is illegal in an iterator block—you must use yield break instead.

Composing Sequences

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.

Composing sequences

Figure 1-5. Composing sequences

The composability of the iterator pattern is extremely useful in building LINQ queries.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset