Chapter 8. Enumerators

Arrays and collections were introduced in Chapter 5—both of these are collection types. Collections group related types, even if the types are related only through inheritance from System.Object. You could maintain a dynamic array of Employee types, which would include instances of HourlyEmployee, ExemptEmployee, and CommissionedEmployee types. You could have a queue of bank requests, such as DepositTransaction, WithdrawalTransaction, and InterestTransaction objects. The relationship of these classes is defined by inheritance at compile time, which allows polymorphism at run time. Enumerating such collections of related objects is a frequent and valuable behavior in many applications.

Enumeration is the process of iterating the items in a collection. A report that lists new employees would need to enumerate the employee collection. Generating a bank statement requires enumerating transactions of a bank account holder.

Because enumeration is a valuable tool to almost every developer and across a myriad of types, some standardization is helpful. Types that are enumerable implement the IEnumerator and IEnumerable interfaces. A generic implementation is available with the IEnumerator<T> and IEnumerable<T> interfaces. IEnumerable interfaces return an enumerator object. The enumerator object implements the IEnumerator interface, which is used to perform the iteration. Collections and arrays support enumeration and implement the enumeration interfaces.

Most types you would want to enumerate have some sort of interface that abstracts the details of enumeration. For example, the ArrayList type has indexes, the BitArray type has the Get method, and Hashtables have the Keys and Values properties. As enumerable types, collections also offer standard enumeration through the IEnumerator interface. Collections are iterated in loops. Do and while loops are prone to logic errors. The foreach statement helps alleviate some of these errors, where the enumeration is implicit. This reduces the code necessary to iterate a collection and makes the code more robust.

The following is a straightforward foreach loop. The colors array is an array of strings. Arrays are collections and are therefore enumerable. Instead of writing a for loop, with indexes and a counter to manage, the foreach loop keeps everything simple—no indexes, and no counters. The foreach loop automatically enumerates each element of the collection:

string[] colors = {"red", "green", "blue"};
foreach (string color in colors) {
    Console.WriteLine(color);
}

Here is another example of a foreach loop. This code manipulates a Stack collection—a more complex structure than a simple array. However, stacks are also collections, meaning that the foreach statement is applicable:

Stack<int> collection = new Stack<int>();
collection.Push(10);
collection.Push(15);
collection.Push(20);
foreach (int number in collection) {
    Console.WriteLine(number);
}

Enumerable Objects

Enumerable objects implement the IEnumerable interface. The GetEnumerator method is the only member of this interface. It returns an enumerator, which is used to enumerate the collections.

This is the IEnumerable interface:

public interface IEnumerable {
    IEnumerator GetEnumerator();
}

Each invocation of the GetEnumerator method returns a unique enumerator. The enumerator is a state machine that maintains a static view of the target collection and a cursor. The cursor points to the current item of the collection. What happens if a collection is modified while being enumerated? An exception should occur. You could lock the collection during enumeration, but that can cause substantial performance degradation. As a best practice, an enumerator should capture the collection as a snapshot. In addition, the snapshot collection is read-only. Depending on the size of the collection, creating a snapshot could stress memory resources. Finally, the GetEnumerator method should be thread-safe and guarantee the return of a unique enumerator, which references an isolated collection regardless of the thread context.

Enumerators

Enumerators are part of the enumeration pattern and normally are implemented as a nested class within the collection type. The primary benefit of nested classes is access to the private members of the outer class. This access allows you to avoid breaking the rules of encapsulation, while allowing the enumerator class to access the data store of the collection. The data store is likely a private member of the collection class and not an external entity.

Enumerators implement the IEnumerator interface, which has three members. This is the IEnumerator interface:

public interface IEnumerator {
    object Current { get; }
    bool MoveNext();
    void Reset();
}

The Current property returns the current element of the collection. MoveNext moves the current element to the next item in the collection. If the iteration has completed, MoveNext returns false. Otherwise, MoveNext returns true. Notice that there is no MovePrevious method; enumeration is forward-only. The Reset method returns the enumeration to the beginning, before the first element of the collection.

The enumerator is the state machine representing the enumeration. Part of the state machine is the cursor, which either directly or indirectly references the current element. The cursor is not necessarily an integral value, but normally it is. For example, the cursor within a linked list might be a node object. When the enumerator is created, the cursor initially points before the first element of the collection. Do not access the Current property while the cursor is in this initial state. Call MoveNext first, which positions the cursor at the first element of the collection.

The following is a typical constructor of an enumerator. In this example, the constructor makes a snapshot of the collection. For reasons of simplicity, the collection in this example is a basic array. The cursor is therefore set to –1, which is before the first element of the collection:

private int cursor;
private object[] elements = null;

public Enumerator(object[] items) {
    elements = new object[items.Length];
    Array.Copy(items, elements, items.Length);
    cursor = -1;
}

The MoveNext method moves the cursor to the next item. The Current property then can be used to return that element. If the list has been fully iterated, MoveNext returns false. Collections are not circular, where MoveNext can cycle through a collection. The Current property is not valid after the collection has been fully enumerated. For this reason, do not use the Current property after MoveNext returns false.

Here is one possible implementation of the MoveNext method:

public bool MoveNext() {
    ++cursor;
    if (cursor > (elements.Length - 1)) {
        return false;
    }
    return true;
}

The Reset method resets the enumeration. The cursor is updated to point to before the collection again.

Here is a Reset method:

public void Reset() {
    cursor = -1;
}

Current is a read-only property. Implement the get method of the property but not the set method. The implementation should check for fencepost errors. If the index is before or after the collection, the appropriate exception should be thrown.

Here is an implementation of the Current property:

public object Current {
    get {
        if (cursor > (elements.Length - 1)) {
            throw new InvalidOperationException(
                "Enumeration already finished");
        }
        if (cursor == -1) {
            throw new InvalidOperationException(
                "Enumeration not started");
        }
        return elements[cursor];
    }
}

Enumerator states

Enumerators can be in one of four possible states. Table 8-1 lists the enumerator states.

Table 8-1. Enumerator states

State

Description

Before

This is the state of the enumerator before enumeration has started or after it has been reset. The Current property is not available. The first call to MoveNext changes the state from Before to Running.

Running

This is the state when MoveNext is calculating the next element of the iteration. When MoveNext returns true, the next element has been enumerated, and the state changes to Suspended. If MoveNext returns false, the state changes to After.

Suspended

The state of the enumerator between enumerations. Calling MoveNext changes the state from Suspended to Running.

After

This is the state after enumeration has completed. The Current property is no longer available. Reset returns the enumeration to Before, and enumeration can be restarted.

An Enumerator Example

Here is sample code for an enumerator class and a consumer. This is also the complete listing for some of the partial code presented earlier in this chapter. The SimpleCollection is a thin wrapper for a static array. Actually, it is somewhat redundant because arrays are already fully functional collections, including exposing an enumerator. However, this simple example is ideal for demonstrating the enumerator pattern.

The enumerator pattern recommends isolation of the underlying collection. In this code, the enumerator is created as a nested class, in which a snapshot of the collection is made in the constructor. The isolated collection is a copy of the array. In addition, the Current property is read-only, which prevents changes to the collection data.

In Main, an instance of SimpleCollection is created. It is initialized with an integer array. The collection is then iterated using the IEnumerator interface:

using System;
using System.Collections;

namespace Donis.CSharpBook {

    public class Starter {

        public static void Main() {
            SimpleCollection simple = new SimpleCollection(
                 new object[] {1,2,3,4,5,6,7});

            IEnumerator enumerator = simple.GetEnumerator();
            while (enumerator.MoveNext()) {
                Console.WriteLine(enumerator.Current);
            }
        }
    }

    public class SimpleCollection: IEnumerable {

        public SimpleCollection(object[] array) {
            items = array;
        }

        public IEnumerator GetEnumerator() {
            return new Enumerator(items);
        }

        private class Enumerator: IEnumerator {

            public Enumerator(object[] items) {
                 elements = new object[items.Length];
                 Array.Copy(items, elements, items.Length);
                 cursor = -1;
            }

            public bool MoveNext() {
                ++cursor;
                if (cursor > (elements.Length - 1)) {
                    return false;
                }
                return true;
           }

           public void Reset() {
               cursor = -1;
           }

           public object Current {
               get {
                   if (cursor > (elements.Length - 1)) {
                       throw new InvalidOperationException(
                           "Enumeration already finished");
                   }
                   if (cursor == -1) {
                       throw new InvalidOperationException(
                           "Enumeration not started");
                   }
                   return elements[cursor];
               }
           }

           private int cursor;
           private object[] elements = null;
        }

        private object[] items = null;
    }
}

As shown, the SimpleCollection class makes a copy of the collection in the enumerator. This isolates the collection from changes in the target array. This is the recommended approach for collections that can be changed. If the collection is static, a copy might not be necessary. Isolation protects a collection against changes during iteration.

An Enumerator Example (Versioned Collection)

The following code offers another implementation of an enumerable class. In this example, a versioned collection is used. A private field called version has been added to the collection class. An indexer has been added to allow clients to modify the collection. The version is incremented whenever the collection is modified. A version number also has been added to the enumerator, which is the nested class. In the constructor, the version is initialized to the version of the outer collection. When the Current property is accessed, the version number inside the enumerator is compared to that of the collection. If unequal, the collection has been modified since the enumerator was created and an exception is raised. This is the implementation model of collections in the .NET Framework Class Library (FCL), such as the ArrayList, Stack, and Queue collections.

The Main method tests the versioning. The collection is modified in Main after the enumerator has been obtained. After the modification, the Current property is used, which triggers the expected exception:

using System;
using System.Collections;

namespace Donis.CSharpBook {

    public class Starter {

        public static void Main() {
            SimpleCollection simple = new SimpleCollection(
                 new object[] {1,2,3,4,5,6,7});

            IEnumerator enumerator = simple.GetEnumerator();
            enumerator.MoveNext();
            Console.WriteLine(enumerator.Current);
            enumerator.MoveNext();
            simple[4] = 10;
            Console.WriteLine(enumerator.Current);  // Exception raised
            enumerator.MoveNext();
        }
    }

    public class SimpleCollection: IEnumerable {

        public SimpleCollection(object[] array) {
            items = array;
            version = 1;
        }

        public object this[int index] {
            get {
                return items[index];
            }
            set {
                ++version;
                items[index] = value;
            }
        }

        public IEnumerator GetEnumerator() {
            return new Enumerator(this);
        }

        private class Enumerator: IEnumerator {

            public Enumerator(SimpleCollection obj) {
                 oThis = obj;
                 cursor = -1;
                 version = oThis.version;
            }

            public bool MoveNext() {
                ++cursor;
                if (cursor > (oThis.items.Length - 1)) {
                    return false;
                }
                return true;
            }

            public void Reset() {
                cursor = -1;
            }

            public object Current {
                get {
                    if (oThis.version != version) {
                        throw new InvalidOperationException(
                            "Collection was modified");
                    }

                    if (cursor > (oThis.items.Length - 1)) {
                        throw new InvalidOperationException(
                            "Enumeration already finished");
                    }
                    if (cursor == -1) {
                        throw new InvalidOperationException(
                            "Enumeration not started");
                    }
                    return oThis.items[cursor];
                }
            }

            private int version;
            private int cursor;
            private SimpleCollection oThis;
        }

        private object[] items=null;
        private int version;
    }
}

IEnumerator Problem

Different techniques to implement the enumerator pattern have been shown. They share a common problem. Enumerators, which implement the IEnumerator interface, iterate System.Object types. The result is the following:

  • There is a performance penalty from boxing and unboxing value types.

  • There is a small performance cost for downcasting (casting from a parent type to a child type) to reference types.

  • Frequent boxing can stress the managed heap.

  • Boxing large collections of value types can also stress the managed heap.

  • Casting to and from System.Object is required, which is not entirely type-safe.

These problems were identified in Chapter 7. The solution was to use generic types. That is the solution here as well. The .NET Framework offers generic versions of the IEnumerable and IEnumerator interfaces.

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

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