Recipe 3.27 Implementing Nested foreach Functionality in a Class
Problem
You need a class that contains an array
of objects; each of these objects in turn contains an array of
objects. You want to use a nested foreach loop to
iterate through all objects in both the outer and inner arrays in the
following manner:
foreach (SubSet aSubSet in Set)
{
foreach (Item i in aSubSet)
{
// Operate on Item objects contained in the innermost object collection
// SomeSubSet, which in turn is contained in another outer collection
// called Set
}
}
Solution
Implement
IEnumerable on the top-level class as usual, but
also implement IEnumerable on each of the objects
returned by the top-level enumeration. The following class set
contains an ArrayList of
SubGroup objects, and each
SubGroup object contains an
ArrayList of Item objects:
using System;
using System.Collections;
//-----------------------------------------
//
// The top-level class
//
//-----------------------------------------
public class Set : IEnumerable
{
//CONSTRUCTORS
public Set( ) {}
//FIELDS
private ArrayList setArray = new ArrayList( );
//PROPERTIES
public int Count
{
get{return(setArray.Count);}
}
//METHODS
public IEnumerator GetEnumerator( )
{
return(new SetEnumerator(this));
}
public int AddGroup(string name)
{
return(setArray.Add(new SubGroup(name)));
}
public SubGroup GetGroup(int setIndex)
{
return((SubGroup)setArray[setIndex]);
}
//NESTED ITEMS
public class SetEnumerator : IEnumerator
{
//CONSTRUCTORS
public SetEnumerator(Set theSet)
{
setObj = theSet;
}
//FIELDS
private Set setObj;
private int index = -1;
//METHODS
public bool MoveNext( )
{
index++;
if (index >= setObj.Count)
{
return(false);
}
else
{
return(true);
}
}
public void Reset( )
{
index = -1;
}
public object Current
{
get{return(setObj.setArray[index]);}
}
}
}
//-----------------------------------------
//
// The inner class
//
//-----------------------------------------
public class SubGroup : IEnumerable
{
//CONSTRUCTORS
public SubGroup( ) {}
public SubGroup(string name)
{
subGroupName = name;
}
//FIELDS
private string subGroupName = "";
private ArrayList itemArray = new ArrayList( );
//PROPERTIES
public string SubGroupName
{
get{return(subGroupName);}
}
public int Count
{
get{return(itemArray.Count);}
}
//METHODS
public int AddItem(string name, int location)
{
return(itemArray.Add(new Item(name, location)));
}
public Item GetSubGroup(int index)
{
return((Item)itemArray[index]);
}
public IEnumerator GetEnumerator( )
{
return(new SubGroupEnumerator(this));
}
//NESTED ITEMS
public class SubGroupEnumerator : IEnumerator
{
//CONSTRUCTORS
public SubGroupEnumerator(SubGroup SubGroupEnum)
{
subGroup = SubGroupEnum;
}
//FIELDS
private SubGroup subGroup;
private int index = -1;
//METHODS
public bool MoveNext( )
{
index++;
if (index >= subGroup.Count)
{
return(false);
}
else
{
return(true);
}
}
public void Reset( )
{
index = -1;
}
public object Current
{
get{return(subGroup.itemArray[index]);}
}
}
}
//-----------------------------------------
//
// The lowest-level class
//
//-----------------------------------------
public class Item
{
//CONSTRUCTOR
public Item(string name, int location)
{
itemName = name;
itemLocation = location;
}
private string itemName = "";
private int itemLocation = 0;
public string ItemName
{
get {return(itemName);}
set {itemName = value;}
}
public int ItemLocation
{
get {return(itemLocation);}
set {itemLocation = value;}
}
}
Discussion
Building functionality into a class to allow it to be iterated over
using the foreach loop is not extremely difficult;
however, building functionality into embedded classes to allow a
nested foreach idiom to be used requires keeping
careful track of the classes you are building.
The
ability of a class to be used by the foreach loop
requires the use of two interfaces: IEnumerable
and IEnumerator. The
IEnumerable interface contains the
GetEnumerator method, which accepts no parameters
and returns an enumerator object. It is this enumerator object that
implements the IEnumerator interface. This
interface contains the methods MoveNext and
Reset along with the property
Current. The MoveNext method
accepts no parameters and returns a bool
indicating whether the MoveNext method has reached
the last element in the collection. The Reset
method also accepts no parameters and returns a
void. This method simply moves to the position in
the collection that is immediately before the first element. Once in
this state, the MoveNext method must be called in
order to access the first element. The Current
property is read-only and returns an object, which is the current
element in the collection.
The code for this recipe is divided among five classes. The top-level
class is the Set class, which contains an
ArrayList of SubGroup objects.
The SubGroup object also contains an
ArrayList, but this ArrayList
contains Item objects. The Set
and SubGroup classes each contain a nested class,
which is the enumerator class (i.e., it implements
IEnumerator). The Set and
SubGroup classes both implement the
IEnumerable interface. The class structure looks
like this:
Set (Implements IEnumerable)
SetEnumerator (Implements IEnumerator and is nested within the Set class)
SubGroup (Implements IEnumerable)
SubGroupEnumerator (Implements IEnumerator and is nested within the SubGroup
class)
Item
By examining the Set class, we can see how classes
usable by a foreach loop are constructed. This
class contains:
A simple ArrayList, which will be iterated over by
the class enumerator. A property, Count, which returns the number of
elements in the ArrayList. A method, GetEnumerator, which is defined by the
IEnumerable interface. This method returns a
SetEnumerator object. As you shall see later, this
object allows the foreach loop to do its work. A method, AddGroup, which adds a
SubGroup object to the
ArrayList. A method, GetGroup, which returns a
SubGroup object in the
ArrayList.
The SetEnumerator class, which is nested within
the Set class, contains:
A constructor that accepts a Set object. This
Set object will be iterated over by the
foreach loop. Two fields to hold the current index (index) and
the Set object (setObj). A method, MoveNext, which is defined by the
IEnumerator interface. This method moves the
current index (index) to the next position in the
Set object's
ArrayList. If the index is moved past the last
element in the ArrayList, a
false is returned. Otherwise, the index is
incremented by one, and a true is returned. A method Reset, which is defined by the
IEnumerator interface. This method moves the
current index (index) to a position immediately
before the first element in the ArrayList of the
Set object (i.e., -1). A method Current, which is defined by the
IEnumerator interface. This method returns the
SubGroup object in the Set
object's ArrayList, which is
pointed to by the index field.
To create the SubGroup and
SubGroupEnumerator class, we follow the same
pattern, except that the SubGroup class contains
an ArrayList of Item objects
and the SubGroupEnumerator operates on a
SubGroup object.
The final class is the Item class. This class is
the lowest level of this structure and contains data that has been
grouped within the SubGroup objects, all of which
is contained in the Set object. There is nothing
out of the ordinary with this class; it simply contains data and the
means with which to set and retrieve this data.
Using these classes is quite simple. The following method shows how
to create a Set object that contains multiple
SubGroup objects, which, in turn, contain multiple
Item
objects:
public void CreateNestedObjects( )
{
Set topLevelSet = new Set( );
// Create two groups under the TopLevelSet object
topLevelSet.AddGroup("sg1");
topLevelSet.AddGroup("sg2");
// For each SubGroup object in the TopLevelSet object, add two Item objects
foreach (SubGroup SG in TopLevelSet)
{
SG.AddItem("item1", 100);
SG.AddItem("item2", 200);
}
}
The CreateNestedObjects method first creates a
topLevelSet object and creates two
SubGroups within it called sg1
and sg2. Each of these SubGroup
objects in turn is filled with two Item objects
called item1 and item2.
The next method shows how to read all of the Item
objects contained within the Set object that was
created in the CreateNestedObjects
method:
public void ReadNestedObjects(Set TopLevelSet)
{
Console.WriteLine("TopLevelSet.Count: " + TopLevelSet.Count);
// Outer foreach to iterate over all SubGroup objects
//in the Set object
foreach (SubGroup SG in TopLevelSet)
{
Console.WriteLine("\tSG.SubGroupName: " + SG.SubGroupName);
Console.WriteLine("\tSG.Count: " + SG.Count);
// Inner foreach to iterate over all Item objects
//in the current SubGroup object
foreach (Item i in SG)
{
Console.WriteLine("\t\ti.ItemName: " + i.ItemName);
Console.WriteLine("\t\ti.ItemLocation: " + i.ItemLocation);
}
}
}
This method displays the following:
TopLevelSet.Count: 2
SG.SubGroupName: sg1
SG.Count: 2
I.ItemName: item1
I.ItemLocation: 100
I.ItemName: item2
I.ItemLocation: 200
SG.SubGroupName: sg2
SG.Count: 2
I.ItemName: item1
I.ItemLocation: 100
I.ItemName: item2
I.ItemLocation: 200
The outer foreach loop is used to iterate over all
SubGroup objects that are stored in the top-level
Set object. The inner foreach
loop is used to iterate over all Item objects that
are stored in the current SubGroup object.
See Also
See the "IEnumerable Interface" and
"IEnumerator Interface" topics in
the MSDN documentation.
|