Using Enumerators

Using Enumerators

Although the version of the AssociativeArray class presented in the previous section is useful, it has a limitation that restricts its use in a C# application. Because it doesn’t support the standard enumeration pattern used by classes in the .NET Framework, iterating over all elements in the array is difficult and requires client code to know the key for each element stored in the collection.

There are multiple ways that iteration over an associative array collection could be supported. One approach would be to expose an indexer that supports numerical indexing, allowing a loop similar to the following to be written:

for(int n = 0; n < foodFavorites.Length; ++n)
{
    string favorite = foodFavorites[n];
    
    

}

The problem with this approach is that it can’t be generalized to all types of collections. For example, consider a collection class that stores items in a tree-based data structure. Although it would be possible to expose an indexer that provides indexing based on integer keys, it wouldn’t result in a natural access method for the container.

The .NET Framework offers a different approach for iterating over all items in a collection—the enumerator. Enumerators are classes used to provide a standard iteration method over all items stored in a collection. By specifying the standard interfaces used to implement enumerators, the .NET Framework formalizes a design pattern that’s used for all collection types. In this section, we’ll examine the standard interfaces used to build enumerators, and we’ll add enumerator support to the AssociativeArray class.

Understanding Enumeration Interfaces

Enumerators are classes that implement the IEnumerator interface, which is part of the System.Collections namespace. The IEnumerator interface, shown in the following code, is used to define a cursor-style iteration over a collection:

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

The IEnumerator interface exposes two methods and one property:

  • Reset  Returns the enumerator to its initial state

  • MoveNext  Moves to the next item in the collection, returning true if the operation was successful or false if the enumerator has moved past the last item

  • Current  Returns the object to which the enumerator currently refers

The IEnumerator interface isn’t implemented directly by collection classes; instead, it’s implemented by separate enumerator classes that provide enumeration functionality. Decoupling enumeration classes from the collection class makes it easy to support multiple simultaneous enumeration objects for one collection, as shown in Figure 7-1. If IEnumerator was to be implemented by a collection class, it would be difficult for a collection class to support more than one client using enumeration at any given time.

Figure 7-1.
Multiple enumeration objects acting on a collection class concurrently.

Classes that support enumeration and want to expose enumerators to their clients implement the IEnumerable interface. IEnumerable includes a single method, GetEnumerator, which returns an enumerator object for the current object. The declaration for IEnumerable is shown here:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

When enumerators are newly minted, the enumerator’s Current property can’t be used because it’s positioned just prior to the first item. To advance the enumerator to the first item in the collection, you must call MoveNext, which will return true if the enumerator is positioned at a valid collection item. This behavior allows you to write a very compact loop when iterating over a collection, as shown here:

IEnumerator enumerator = enumerable.GetEnumerator();
while(enumerator.MoveNext())
{
    MyObject obj = (MyObject)enumerator.Current;
    
    

}

Enumerators are always tightly bound to a specific collection object. If the collection is updated, any enumerators associated with the collection are invalidated and can’t be used. If you attempt to use an enumerator after it’s been invalidated or when it’s positioned before the first element or after the last element, an InvalidOperationException exception will be thrown.

Implementing Enumeration Interfaces

The first step in implementing enumeration interfaces is to create an internal or embedded class that implements IEnumerator. Although you’ll create a concrete class derived from IEnumerator to implement the enumerator, you probably won’t expose this class as public. To prevent clients from depending on your specific enumerator declaration, it’s a much better idea to keep your enumerator class protected or private and expose functionality only through the IEnumerator interface.

Creating an Enumerator Class

In addition to implementing the IEnumerator interface, your enumerator class must be able to access individual items in the underlying collection. For the enumerator example presented in this section, the AssociativeArray class has been modified to make the _items field internal, granting access to any classes in the assembly, as shown here:

internal object[] _items;

Another approach is to implement an enumerator as a class that’s embedded in the collection class, as follows:

public class AssociativeArray: IEnumerable
{
    
    

    public class AssociativeArrayEnumerator : IEnumerator
    {
    
    

    }
}

When an enumerator is implemented as a class embedded within the collection, it can access all members of the collection class, which can provide for better encapsulation. However, embedding the enumerator also increases the size and complexity of the collection class. For the purposes of this example, the enumerator class is broken out separately.

Regardless of the implementation of the enumerator, you’ll need to pass a reference to the collection to the enumerator when the enumerator is constructed and initialized, as shown here:

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

Remember that you must provide a mechanism that enables the collection to invalidate any enumerators that are associated with the collection. This is the only way that users of the enumerator can determine that the collection has been updated. A good way to invalidate the enumerator is to provide an event that’s raised when the collection is updated. By subscribing to the event during construction, enumerators can receive an event notification and mark themselves invalid when their associated collection is changed.

An example of an enumerator that provides iteration access to the AssociativeArray class is shown here:

// An enumerator for the AssociativeArray class
public class AssociativeArrayEnumerator : IEnumerator
{
    public AssociativeArrayEnumerator(AssociativeArray ar)
    {
        _ar = ar;
        _currIndex = -1;
        _invalidated = false;
        // Subscribe to collection change events.
        AssociativeArray.ChangeEventHandler h;
        h = new AssociativeArray.ChangeEventHandler(InvalidatedHandler);
        ar.AddOnChanged(h);
    }
    // Property that retrieves the element in the array that this
    // enumerator instance is pointing to. If the enumerator has
    // been invalidated or isn't pointing to a valid item, throw
    // an InvalidOperationException exception.
    public object Current
    {
        get
        {
            AssociativeArray.KeyItemPair pair;
            if(_invalidated ¦¦ 
                _currIndex == -1 ¦¦
                _currIndex == _ar.Length)
                throw new InvalidOperationException();
            pair = (AssociativeArray.KeyItemPair)_ar._items[_currIndex];
            return pair.item;
        }
    }
    // Move to the next item in the collection, returning true if the
    // enumerator refers to a valid item and returning false otherwise.
    public bool MoveNext()
    {
        if(_invalidated ¦¦ _currIndex == _ar._items.Length)
            throw new InvalidOperationException();
        _currIndex++;
        if(_currIndex == _ar.Length)
            return false;
        else
            return true;
    }
    // Reset the enumerator to its initial position.
    public void Reset()
    {
        if(_invalidated)
            throw new InvalidOperationException();
        _currIndex = -1;
    }
    // Event handler for changes to the underlying collection. When
    // a change occurs, this enumerator is invalidated and must be
    // re-created.
    private void InvalidatedHandler(object sender, EventArgs e)
    {
        _invalidated = true;
    }
    // Flag that marks the collection as invalid after a change to
    // the associative array
    protected bool _invalidated;
    // The index of the item this enumerator applies to
    protected int _currIndex;
    // A reference to this enumerator's associative array
    protected AssociativeArray _ar;
}
Providing Access to the Enumerator

You should create new enumerator objects in response to a call to the collection class’s GetEnumerator method. Although you’ll return your enumerator object, the client code that retrieves your enumerator has access to your object only through IEnumerator because GetEnumerator is typed to return only an IEnumerator interface. The version of GetEnumerator for the AssociativeArray class is shown here:

public class AssociativeArray: IEnumerable
{
    
    

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

}

In response to the call to GetEnumerator, the AssociativeArray class creates a new enumerator object, passing the this pointer so that the enumerator can initialize itself to refer to this AssociativeArray object.

Other changes made to the AssociativeArray class to provide enumerator support are shown here:

public class AssociativeArray: IEnumerable
{
    // Event delegate 
    public delegate void ChangeEventHandler(object sender, EventArgs e);
    
    

    public AssociativeArray(int initialSize)
    {
        _count = 0;
        _items = new object[initialSize];
        _eventArgs = new EventArgs();
    }
    
    

    public void AddOnChanged(ChangeEventHandler handler)
    {
        Changed += handler;
    }
    // Remove an event handler for the changed event.
    public void RemoveOnChanged(ChangeEventHandler handler)
    {
        Changed -= handler;
    }
    // Raise a changed event to subscribed enumerators.
    protected void OnChanged()
    {
        if(Changed != null)
            Changed(this, _eventArgs);
    }
    
    

    // Event handler for distributing change events to enumerators
    public event ChangeEventHandler Changed;
    // A member that holds change event arguments
    protected EventArgs _eventArgs;
}

In addition to implementing IEnumerable, several changes have been made to the AssociativeArray class. First, an event delegate type has been declared to propagate events to enumerators. Next, the AddOnChanged and RemoveOnChanged methods have been added to assist in subscribing and unsubscribing to the Changed event. Each enumerator subscribes to this event so that the enumerators can be invalidated after changes to the AssociativeArray object. Instead of creating new EventArgs objects when each Changed event is raised, a single EventArgs object is created and passed for all events raised during the object’s lifetime.

Consuming Enumeration Interfaces

There are two ways to use the .NET Framework’s enumerator interfaces. The first method is to request the enumerator directly and explicitly iterate over the collection using the IEnumerator interface, like this:

public void Iterate(object obj)
{
    IEnumerable enumerable = obj as IEnumerable;
    if(enumerable != null)
    {
        IEnumerator enumerator = enumerable.GetEnumerator();
        while(enumerator.MoveNext())
        {
            object theObject = enumerator.Current;
            Console.WriteLine(theObject.ToString());
        }
    }
}

A more useful and common way to make use of the enumeration interfaces is to use a foreach loop, as shown in the following code.

static void Main(string[] args)
{
    AssociativeArray foodFavorites = new AssociativeArray(4);
    foodFavorites["Mickey"] = "Risotto with Wild Mushrooms";
    foodFavorites["Ali"] = "Plain Cheeseburger";
    foodFavorites["Mackenzie"] = "Macaroni and Cheese";
    foodFavorites["Rene"] = "Escargots";

    try
    {
        foreach(string food in foodFavorites)
        {
            Console.WriteLine(food);
        }
    }
    catch(InvalidOperationException exc)
    {
        Console.WriteLine(exc.ToString());
    }
}

Behind the scenes, the C# compiler will generate the code required to make use of IEnumerable and IEnumerator. When the preceding code is compiled and executed, the output looks like this:

Risotto with Wild Mushrooms
Plain Cheeseburger
Macaroni and Cheese
Escargots


Part III: Programming Windows Forms