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;
}