Recipe 3.34 Creating Custom Enumerators
Problem
You need to add
foreach support to a class, but the normal way of
adding an IEnumerator class is not flexible
enough. Instead of simply iterating from the first element to the
last, you also need to iterate from the last to the first, and you
need to be able to step over, or skip, a predefined number of
elements on each iteration. All of these types of iterators should be
available to your class.
Solution
The following interfaces allow polymorphic use of the
foreach method:
using System.Collections;
public interface IRevEnumerator
{
IEnumerator GetEnumerator( );
}
public interface IStepEnumerator
{
IEnumerator GetEnumerator( );
}
The following class acts as a container for a private
ArrayList called InternalList
and is used in the foreach loop to iterate through
the private
InternalList:
public class Container : IEnumerable, IRevEnumerator, IStepEnumerator
{
public Container( )
{
// Add dummy data to this class
internalList.Add(-1);
internalList.Add(1);
internalList.Add(2);
internalList.Add(3);
internalList.Add(4);
internalList.Add(5);
internalList.Add(6);
internalList.Add(7);
internalList.Add(8);
internalList.Add(9);
internalList.Add(10);
internalList.Add(200);
internalList.Add(500);
}
private ArrayList internalList = new ArrayList( );
private int step = 1;
IEnumerator IEnumerable.GetEnumerator( )
{
return (new ContainerIterator(this));
}
IEnumerator IRevEnumerator.GetEnumerator( )
{
return (new RevContainerIterator(this));
}
IEnumerator IStepEnumerator.GetEnumerator( )
{
return (new StepContainerIterator(this, step));
}
public int ForeachStep
{
get {return (step);}
set {step = value;}
}
public ArrayList List
{
get {return (internalList);}
set {internalList = value;}
}
// Nested classes
// This class iterates from the first element to the last element
// in the internalList
public class ContainerIterator : IEnumerator
{
public ContainerIterator(Container c)
{
this.c = c;
Reset( );
}
private int index = -1;
private Container c = null;
public void Reset( )
{
index = -1;
}
public object Current
{
get {return (c.internalList[index]);}
}
public bool MoveNext( )
{
++index;
if (index < c.internalList.Count)
{
return (true);
}
else
{
return (false);
}
}
}
// This class iterates from the last element to the first element
// in the internalList
public class RevContainerIterator : IEnumerator
{
public RevContainerIterator(Container c)
{
this.c = c;
Reset( );
}
private int index = -1;
private Container C = null;
public void Reset( )
{
index = c.internalList.Count;
}
public object Current
{
get {return (c.internalList[index]);}
}
public bool MoveNext( )
{
--index;
if (index >= 0)
{
return (true);
}
else
{
return (false);
}
}
}
// This class iterates from the first element to the last element
// int the internalList and skips a predefined number of elements
// in the internalList on each iteration
public class StepContainerIterator : IEnumerator
{
public StepContainerIterator(Container c, int step)
{
this.c = c;
this.step = step;
Reset( );
}
private int index = -1;
private int step = 1;
private Container c = null;
public void Reset( )
{
index = -1;
}
public object Current
{
get {return (c.internalList[index]);}
}
public bool MoveNext( )
{
if (index == -1)
{
++index;
}
else
{
index += step;
}
if (index < c.internalList.Count)
{
return (true);
}
else
{
return (false);
}
}
}
}
Discussion
The iterator design
pattern provides an easy method of moving from item to
item contained within an object. This object could be an array, a
collection, or some other similar type of container. This technique
is similar to using a for loop to iterate over
each item contained in an array. The difference is that, with the
iterator design pattern, you do not need advance knowledge about how
the elements are stored in the container or where the elements are
located in the container. In contrast, when using a
for loop, you need to know what elements are
stored in the container. And you need some type of direct or indirect
access, via a method or indexer, to the contained list of elements.
This is not so with the foreach loop.
The
FCL provides two special interfaces, IEnumerable
and IEnumerator, that allow us to easily implement
this design pattern. The IEnumerable interface
defines a single method:
IEnumerator GetEnumerator( )
This method accepts no parameters and returns an
IEnumerator interface object. The
IEnumerator type that is returned is another
interface that has the following property and two methods:
object Current {get;}
bool MoveNext( )
void Reset( )
The Current method returns the current element
being accessed. The MoveNext method moves to the
next element. If this method successfully moves to the next element,
it returns true. If there are no more elements in
the list, it returns false as an indication to
stop the iteration. The Reset method resets the
current element pointer to the position immediately before the first
element in the list.
Implementing these two interfaces allows us to use a familiar looping
mechanism: the foreach loop. With the
foreach loop, you do not have to worry about
moving the current element pointer to the beginning of the list or
even about incrementing this pointer as you move through the list. In
addition, you do not have to watch for the end of the list,
preventing you from going beyond the bounds of the list. The best
part about the foreach loop and the iterator
pattern is that you do not have to know how to access the list of
elements within its container—indeed, you do not even have to
have access to the list of elements; the
IEnumerator and IEnumerable
interfaces implemented on the container do this for you.
The Container class contains a private
ArrayList of items called
internalList. The GetEnumerator
method on this class is implemented to return a class called
ContainerIterator, which is nested within the
Container class. This
ContainerIterator class implements the
IEnumerator interface and contains all of the
intelligence to control how the foreach loop
operates and what data it operates on.
The ContainerIterator class uses a private
variable, index, as the pointer to the current
element in the Container.internalList
ArrayList. Remember that this
ArrayList is private and cannot be seen by the
client code. A second private variable, c, holds a
pointer to the outer class Container. A nested
class is used because it can see all private members of the outer
class; therefore, it is not a problem to access the
internalList from the nested
ContainerIterator class.
The ContainerIterator class is designed to move
from the first element in the ArrayList to the
last. The foreach loop may not change this.
However, we may add code to the ContainerIterator
class to move across the elements in the ArrayList
in different manners. For example, if we always wanted to iterate the
ArrayList in ascending order, we could modify the
ContainerIterator class constructor by calling the
ArrayList.Sort method, as follows:
public ContainerIterator(Container c)
{
this.c = c;
Reset( );
c.internalList.Sort( );
}
The ContainerIterator may also be modified to
start at the last element in the ArrayList and
work its way to the first element. The
RevContainerIterator class demonstrates this. A
third class called StepContainerIterator
demonstrates a way to step over a specified number of elements in the
ArrayList. This is similar to the following VB.NET
and C# code:
' VB.NET Code
For i = 0 to 100 Step 2
...
Next
// C# Code
for (int i = 0; i <= 100; i += 2) {...}
In both of these loops, every other element in the list is skipped.
The StepContainerIterator allows this by accepting
an integer value in its constructor that determines how many items
will be skipped. This is the step
parameter in this constructor.
There is also a way to choose which IEnumerator
interface to use with a foreach loop. Since the
GetEnumerator method returns only an
IEnumerator interface instead of a concrete
object, we can nest all three of these IEnumerator
type classes within our Container class and tell
the foreach loop which iterator it will use.
To enable this, our Container class needs to
implement three distinct interfaces, IEnumerator,
IRevEnumerator, and
IStepEnumerator. The
IEnumerator interface is defined by the FCL.
Notice that all three interfaces define the same
GetEnumerator method. The
Container class implements the methods on these
three interfaces as explicit interface methods. This allows us to
choose which GetInterface method to use by casting
the Container class to one of these three
interface types.
To use the ContainerIterator, we do not have to do
anything to the foreach loop; this is the default
IEnumerator type that is returned by
GetEnumerator. The code for this is as follows:
Container cntnr = new Container( );
foreach (int i in cntnr)
{
Console.WriteLine(i);
}
If we do not know that cntnr contains an
ArrayList of integers, we could write the
following to iterate over each element:
foreach (object i in cntnr)
{
Console.WriteLine(i.ToString( ));
}
To use RevContainerIterator, we cast the
cntnr object to the interface that has the
GetEnumerator method to return a
RevContainerIterator. This code is written as
follows:
Container cntnr = new Container( );
foreach (int i in ((IRevEnumerator)cntnr))
{
Console.WriteLine(i);
}
Again, to use the StepContainerIterator, we cast
the cntnr object to the correct interface,
IStepEnumerator. This code is written as follows:
Container cntnr = new Container( );
cntnr.ForeachStep = 2;
foreach (int i in ((IStepEnumerator)cntnr))
{
Console.WriteLine(i);
}
Notice the extra step with this interface: the
ForeachStep property in the
Container object needs to be set to an integer
value. This value then is passed to the
StepContainerIterator constructor to be used in
skipping over that number of elements in the list.
See Also
See the "IEnumerator Interface,"
"Using foreach with Collections,"
and "Collection Classes Tutorial"
topics in the MSDN documentation.
|