Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implementing custom collection classes with MC++

0.00/5 (No votes)
12 Jun 2002 1  
Tutorial on creating your own collection classes that are enumerable and sortable. Explains in detail the usage of the IEnumerable, IEnumerator, IComparable and IComparer interfaces

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();

//Populate the collection


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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here