Page 66 - MSDN Magazine, June 2017
P. 66
EssEntial .nEt MARK MICHAELIS Custom Iterators with Yield
In my last column (msdn.com/magazine/mt797654), I delved into the details of how the C# foreach statement works under the covers, explaining how the C# compiler implements the foreach capa- bilities in Common Intermediate Language (CIL). I also briefly touched on the yield keyword with an example (see Figure 1), but little to no explanation.
This is a continuation of that article, in which I provide more detail about the yield keyword and how to use it.
Iterators and State
By placing a break point at the start of the GetEnumerator method in Figure 1, you’ll observe that GetEnumerator is called at the start of the foreach statement. At that point, an iterator object is created and its state is initialized to a special “start” state that represents the fact that no code has executed in the iterator and, therefore, no values have been yielded yet. From then on, the iterator maintains its state (location), as long as the foreach statement at the call site continues to execute. Every time the loop requests the next value, control enters the iterator and continues where it left off the pre- vious time around the loop; the state information stored in the iterator object is used to determine where control must resume. When the foreach statement at the call site terminates, the iterator’s state is no longer saved. Figure 2 shows a high-level sequence dia- gram of what takes place. Remember that the MoveNext method appears on the IEnumerator<T> interface.
In Figure 2, the foreach statement at the call site initiates a call to GetEnumerator on the CSharpBuiltInTypes instance called keywords. As you can see, it’s always safe to call GetEnumerator again; “fresh” enumerator objects will be created when necessary. Given the iter- ator instance (referenced by iterator), foreach begins each iteration with a call to MoveNext. Within the iterator, you yield a value back to the foreach statement at the call site. After the yield return state- ment, the GetEnumerator method seemingly pauses until the next MoveNext request. Back at the loop body, the foreach statement dis- plays the yielded value on the screen. It then loops back around and calls MoveNext on the iterator again. Notice that the second time, control picks up at the second yield return statement. Once again, the foreach displays on the screen what CSharpBuiltInTypes yielded and starts the loop again. This process continues until there are no more yield return statements within the iterator. At that point, the foreach loop at the call site terminates because MoveNext returns false.
Another Iterator Example
Consider a similar example with the BinaryTree<T>, which I intro- duced in the previous article. To implement the BinaryTree<T>, I first need Pair<T> to support the IEnumerable<T> interface using an iterator. Figure 3 is an example that yields each element in Pair<T>.
In Figure 3, the iteration over the Pair<T> data type loops twice: first through yield return First, and then through yield return Second. Each time the yield return statement within GetEnumerator is encountered, the state is saved and execution appears to “jump” out of the GetEnumerator method context and into the loop body. When the second iteration starts, GetEnumerator begins to exe- cute again with the yield return Second statement.
Figure 1 Yielding Some C# Keywords Sequentially
using System.Collections.Generic;
public class CSharpBuiltInTypes: IEnumerable<string> {
public IEnumerator<string> GetEnumerator() {
yield return "object"; yield return "byte"; yield return "uint"; yield return "ulong"; yield return "float"; yield return "char"; yield return "bool"; yield return "ushort"; yield return "decimal"; yield return "int"; yield return "sbyte"; yield return "short"; yield return "long"; yield return "void"; yield return "double"; yield return "string";
}
// The IEnumerable.GetEnumerator method is also required // because IEnumerable<T> derives from IEnumerable.
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
// Invoke IEnumerator<string> GetEnumerator() above. return GetEnumerator();
} }
public class Program {
static void Main() {
var keywords = new CSharpBuiltInTypes(); foreach (string keyword in keywords)
{
Console.WriteLine(keyword); }
} }
Code download available at itl.tc/MSDN.2017.06.
62 msdn magazine