Introduction
A collection class is basically a pool of indexed objects. This means we know
how many objects the collection has at any one time. We can enumerate the
objects one by one. We can access the object associated with a particular index.
Usually collection classes are also sortable. For collection classes to be
sortable, the objects it holds or collects must be comparable among objects of
it's same type. In this tutorial we'll see how to design and create collection
classes of our own. We'll create a class called Student which implements the
IComparable interface. This will serve as the object we collect. We'll
also have a nested class inside the Student class called StudentAgeComparer
which implements the IComparer interface. This class is for sorting purposes.
Then we'll create the class called StudentCollection which is the collection
class for the Student class and which implements the IEnumerable interface. The
StudentCollection class will hold a nested class called StudentEnumerator which
implements the IEnumerator interface. This is the interface that implements all
the required methods and properties that makes a class enumerable. By the way C#
programmers might be interested to know that they can use foreach to enumerate
collection classes that implement IEnumerable and IEnumerator
interfaces.
The IEnumerable interface
IEnumerable is an interface that a collection class implements if it's
members are to be enumerated. The foreach C# keyword can enumerate only those
classes that implement IEnumerable. It's sole purpose is to expose the
enumerator object which is used to enumerate the various objects in the
collection in a sequential order. This enumerator object is nothing but a
pointer to an instance of a class that implements the IEnumerator interface.
Typically when we have a class that implements the IEnumerable interface we have
an inner class that acts as the enumerator class of this class. The inner
enumerator class will implement IEnumerator.
The IEnumerable interface defines
just one single method called GetEnumerator which simply returns the enumerator
for the class. A typical scenario is shown below where we have a collection
class called AbcCollection which implements IEnumerable and a nested inner
class called AbcCollectionEnumerator which implements IEnumerator. The advantage
of making the IEnumerator implementing class a nested inner class of the
IEnumerable
implementing class is that the enumerator will have access to the private fields
and methods of the collection class.
__gc class AbcCollection : public IEnumerable
{
public:
IEnumerator* GetEnumerator()
{
...
}
__gc class AbcCollectionEnumerator : public IEnumerator
{
...
};
};
The IEnumerator interface
Any collection class that allows it's collected items to be enumerated must
implement an enumerator which implements the IEnumerator interface. The
enumerator is initially positioned just before the first item. There is a method
called MoveNext which advances the enumerator to the next object in the
collection. If the position of the enumerator was successfully advanced MoveNext
returns true. If MoveNext failed because the enumerator has reached the end of
the collection, then it returns false. If the collection has been modified after
the enumerator was instantiated then MoveNext will throw an
InvalidOperationException exception.
The IEnumerator interface has a
property called Current which returns the current object in the collection that
the enumerator is pointing to. If the enumerator is positioned before the first
element in the collection or if the enumerator has enumerated past the
collection's edge, then an InvalidOperationException exception is thrown. An
InvalidOperationException exception is also thrown if the collection has been
modified after the enumerator has been instantiated. The third and last member
IEnumerator implements is the Reset method which will reset the enumerator to
it's initial position of one before the first object in the collection.
Typically a class that implements the IEnumerator interface has a constructor
that takes an argument. This argument will be of the type of the collection
class, that this class enumerates. There will also be a private member of the
collection class type and the constructor will initialize it with the argument
passed to it.
__gc class AbcCollectionEnumerator : public IEnumerator
{
AbcCollectionEnumerator(AbcCollection* abc)
{
m_privmember = abc;
...
}
...
};
The IComparable interface
Any class that needs to be ordered or sorted will need to implement
IComparable. That is not strictly true in the sense that another class can be
used as a comparer for objects of our class and this comparer class will need to
implement the IComparer interface. But we'll touch that later. Anyway
IComparable defines only one member which is a method called CompareTo.
CompareTo will take an object of the same type as the class that
implements IComparable and will return an int. The returned int
will be 0 if the
object is equal to the current instance, a negative number if the current
instance is less than the object and a positive non-zero number if the current
instance is greater than the object. An array or array list of objects that
implement IComparable are sortable.
The IComparer interface
The IComparer interface provides a method to compare two objects and is used
for sorting collection classes. One of the overrides of the Sort() functions
take an IComparer object as argument and will use this IComparer to compare the
objects that are to be sorted. The IComparer interface defines only one method
called Compare. Similar to the CompareTo method of the IComparable interface,
this returns 0 if both objects are equal. It will return a negative number if
the first object is less than the second object and a non-zero positive number
if the first object is greater than the second object. Typical implementations
are done using the CompareTo method of one or more of the members of the objects
to be compared.
Student class
The Student class that we are going to implement will implement IComparable
so that Student objects may be compared and sorted. In addition we'll also have
an inner nested class called StudentAgeComparer which provides an alternate
sorting based on an alternate criteria from the default one. The Student class
is listed below in an incomplete manner to save space. To see the complete
listing you can take a look at the source code provided with the article or the
full code listing provided at the end of the article.
__gc class Student : public IComparable
{
public:
Student(String* sname, String* srollnum, int sage)
{
...
}
void Display()
{
...
}
int CompareTo(Object* obj)
{
Student* tmp = dynamic_cast<Student*>(obj);
return m_name->CompareTo(tmp->m_name);
}
__gc class StudentAgeComparer : public IComparer
{
...
};
private:
String* m_name;
String* m_rollnum;
int m_age;
};
As you can see, we don't do anything fancy in the CompareTo method. We simply
delegate the comparison to a lower level by calling CompareTo on the m_name
member of the object which is a String. The String class implements
IComparable
and thus we are saved some coding, which is excellent. Thus by default a Student
object collection will be sorted on the basis of the Student object's m_name
member. Now assume that someone says that they want to sort a Student object
collection or array by age. To facilitate for such a contingency we have an
inner class called StudentAgeComparer which implements the IComparer
interface. We'll see it's implementation below.
__gc class StudentAgeComparer : public IComparer
{
public:
int Compare(Object* x,Object* y)
{
Student* x1 = dynamic_cast<Student*>(x);
Student* y1 = dynamic_cast<Student*>(y);
return x1->m_age.ToString()->CompareTo(y1->m_age.ToString());
}
};
Again we delegate the comparison to a lower level. This time we call the
CompareTo function on the String returned by calling ToString() on the
m_age
member. We could have called it directly on the int but then we'd have had to do
boxing on our own, this is MC++ remember, not C#. I thought it best to use the
String forms to do the comparison. Later on when we implement the collection,
we'll see how we implement an alternate sort that uses the StudentAgeComparer to
do sorting based on a student's age instead of on a student's name which is the
default.
StudentCollection class
The StudentCollection class is a collection class of Student objects and
implements IEnumerable. It also has an inner class called StudentEnumerator
which acts as the enumerator for the StudentCollection class. The
StudentCollection class has a private ArrayList member which is used to store
the various Student objects. I chose an ArrayList instead of an
Array
because an ArrayList is dynamically resizable. Thus I don't need to specify a
default size. The ArrayList will keep growing as I keep adding more objects. The
StudentCollection is listed below and as before the listing is only partial.
Look at the zipped source code or the full listing at the end of the article for
the complete listing of code.
__gc class StudentCollection : public IEnumerable
{
public:
StudentCollection()
{
m_students = new ArrayList();
m_dirty = false;
}
__gc class StudentEnumerator : public IEnumerator
{
...
};
IEnumerator* GetEnumerator()
{
m_dirty = false;
return dynamic_cast<IEnumerator*>(new StudentEnumerator(this));
}
int Add(Object* value)
{
m_dirty = true;
return m_students->Add(value);
}
void Sort()
{
m_dirty=true;
m_students->Sort();
}
void SortByAge()
{
m_dirty=true;
m_students->Sort(new Student::StudentAgeComparer());
}
private:
ArrayList* m_students;
bool m_dirty;
};
The GetEnumerator() method returns a new instance of the StudentEnumerator class.
Thus each time we call
GetEnumerator() a new enumerator
is created. If any change is made to the collection we set a dirty flag to true
to indicate to the enumerator that the collection has been changed after the
enumerator has been created. There are two Sort methods provided. The first Sort
method simply calls the zero-argument Sort overloaded method of the ArrayList
class. In this case comparison and sorting can be done on the Student object
because it implements IComparable. There is also an alternate sorting method
called SortByAge where we call another overload of ArrayList's
Sort()
method where we pass it an instance of an object that implements the IComparer
interface. In this case we pass a new instance of the StudentAgeComparer
class. Now the relevance of the IComparable and the IComparer interfaces
must have become clearer to you. Now let's take a look at the StudentEnumerator
class.
__gc class StudentEnumerator : public IEnumerator
{
public:
StudentEnumerator(StudentCollection* sc)
{
m_sc = sc;
index = -1;
}
Object* get_Current()
{
if(index < 0 || index >= m_sc->m_students->Count || m_sc->m_dirty)
throw new InvalidOperationException();
return m_sc->m_students->Item[index];
}
void Reset()
{
m_sc->m_dirty = false;
index = -1;
}
bool MoveNext()
{
if(m_sc->m_dirty)
throw new InvalidOperationException();
index++;
return (index < m_sc->m_students->Count);
}
private:
int index;
StudentCollection* m_sc;
};
The enumerator class has a private member of type StudentCollection and in
the constructor we assign the passed StudentCollection object to this private
member. We also have an index which is by default set to -1 since the ArrayList
index starts from 0. When a call is made to the Reset method we simply set the
index back to -1 and set the dirty flag to false. In the MoveNext() method we
throw an InvalidOperationException exception if the dirty flag is set. Else we
increment index and if we haven't gone past the end of the collection, we return
true, otherwise we return false. Similarly in the get implementation of the
Current property, we check to see if the index is below 0 or if it has crossed
the edge of the collection's limit. In either case or if the dirty flag has been
set an exception of type InvalidOperationException is thrown. Otherwise the
Student object in the ArrayList that has the current index is returned.
Enumerating our collection
StudentCollection* sc = new StudentCollection();
IEnumerator* iEnum = sc->GetEnumerator();
while(i->MoveNext())
{
Student* stmp = dynamic_cast<Student*>(iEnum->Current);
stmp->Display();
}
Screenshot of sample

Full source listing
#include "stdafx.h"
#using <mscorlib.dll>
#include <tchar.h>
using namespace System;
using namespace System::Collections;
__gc class Student : public IComparable
{
public:
Student(String* sname, String* srollnum, int sage)
{
m_name = sname;
m_rollnum = srollnum;
m_age=sage;
}
void Display()
{
Console::WriteLine("{0}{1}{2}",
m_name->PadRight(30),
m_rollnum->PadRight(15),m_age.ToString());
}
int CompareTo(Object* obj)
{
Student* tmp = dynamic_cast<Student*>(obj);
return m_name->CompareTo(tmp->m_name);
}
__gc class StudentAgeComparer : public IComparer
{
public:
int Compare(Object* x,Object* y)
{
Student* x1 = dynamic_cast<Student*>(x);
Student* y1 = dynamic_cast<Student*>(y);
return x1->m_age.ToString()->CompareTo(
y1->m_age.ToString());
}
};
private:
String* m_name;
String* m_rollnum;
int m_age;
};
__gc class StudentCollection : public IEnumerable
{
public:
StudentCollection()
{
m_students = new ArrayList();
m_dirty = false;
}
__gc class StudentEnumerator : public IEnumerator
{
public:
StudentEnumerator(StudentCollection* sc)
{
m_sc=sc;
index = -1;
}
Object* get_Current()
{
if(index <0 || index >= m_sc->m_students->Count ||
m_sc->m_dirty)
throw new InvalidOperationException();
return m_sc->m_students->Item[index];
}
void Reset()
{
m_sc->m_dirty = false;
index=-1;
}
bool MoveNext()
{
if(m_sc->m_dirty)
throw new InvalidOperationException();
index++;
return (index < m_sc->m_students->Count);
}
private:
int index;
StudentCollection* m_sc;
};
IEnumerator* GetEnumerator()
{
m_dirty = false;
return dynamic_cast<IEnumerator*>(
new StudentEnumerator(this));
}
int Add(Object* value)
{
m_dirty = true;
return m_students->Add(value);
}
void Sort()
{
m_dirty=true;
m_students->Sort();
}
void SortByAge()
{
m_dirty=true;
m_students->Sort(new Student::StudentAgeComparer());
}
private:
ArrayList* m_students;
bool m_dirty;
};
int _tmain(void)
{
Student *s1 = new Student("Sally Smith","GF/45/112",22);
Student *s2 = new Student("Alice Brown","GF/45/117",27);
Student *s3 = new Student("Linda Mathews","GF/45/120",23);
StudentCollection* sc = new StudentCollection();
sc->Add(s1);
sc->Add(s2);
sc->Add(s3);
IEnumerator* iEnum = sc->GetEnumerator();
while(iEnum->MoveNext())
{
Student* stmp = dynamic_cast<Student*>(iEnum->Current);
stmp->Display();
}
sc->Sort();
iEnum->Reset();
Console::WriteLine();
while(iEnum->MoveNext())
{
Student* stmp = dynamic_cast<Student*>(iEnum->Current);
stmp->Display();
}
sc->SortByAge();
iEnum->Reset();
Console::WriteLine();
while(iEnum->MoveNext())
{
Student* stmp = dynamic_cast<Student*>(iEnum->Current);
stmp->Display();
}
return 0;
}
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site -
www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff -
blog.voidnish.com.
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy
Summer Love and Some more Cricket as well as a programming book –
Extending MFC applications with the .NET Framework.
Nish's latest book
C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.