Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Implementing C# Generic Collections using ICollection<T>

4.53/5 (25 votes)
8 Nov 2007CPOL10 min read 1   1.9K  
An article explaining a way to implement a generic collection in C# using ICollection, with an example Business Logic Layer.

Screenshot - GenericsExample.jpg

Introduction

In the process of building three-tier web applications, I came across the need for a generic collection class which would be able to hold a type-safe collection of any of my business objects from my Business Logic Layer. I had already written a non-generic collection class (one for each of my business objects, sadly) which inherited from CollectionBase. As the need for more and more business objects arose, I immediately stopped what I was doing and started looking for a more dynamic solution.

As I could recall, C++ had something called template classes that would accept a dynamic variable type. It seemed to me that a generic collection class which would be enumerable (i.e., able to be traversed using a foreach statement) wouldn't be very difficult to implement in C#. I thought the best way to go about this would be to write a generic class that implemented the generic interface ICollection<T>. I set out looking for examples on the Internet, and found that decent examples of this implementation were few and far between, and some sites actually stated that it wasn't worth looking at because no one implements ICollection<T>. Surely, I thought, there must be someone who implements ICollection<T>.

After a bit of searching around, I found a few examples showing incomplete and/or non-functioning code on how to implement this interface. I took what I could learn from all of the information I found, and came up with what I believe to be a decent, functioning implementation of ICollection<T> as well as IEnumerator<T> to enable enumeration through a foreach statement. This article attempts to present a solid example of how to implement ICollection<T> and IEnumerator<T> in order to create a generic, type-safe, and expandable collection class.

What are Generics?

Generics were first introduced into the C# language in .NET 2.0. They provide a type-safe method of accessing data used in collections and/or function calls. Using Generics significantly decreases the amount of run-time errors due to casting or boxing/unboxing because the types used in a generic operations are evaluated at compile time. If you are a C++ fan, you will realize that Generics operate in a very similar way to Template Classes in C++, but with a few differences.

What is the Difference Between C++ Templates and C# Generics?

One of the main differences between C++ Template Classes and C# Generics is that in C#, you cannot use arithmetic operators (except custom operators) in a generic class.

C# does not allow type parameters to have default types, nor does C# allow you to use the type parameter (T) as the base class for the Generic type.

These are only a few of the differences at a glance, but it is important to take note of them if you are coming from a familiar C++ frame of reference. Even though C++ is more flexible in this area, the C# model provides a decreased amount of runtime errors by limiting what can be done based on Generic Constraints.

A Closer Look at ICollection<T>

Before we get started, let's take a look at how ICollection<T> is defined in the .NET Framework.

C#
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
      void Add(T item);

      bool Remove(T item);
      void Clear();
      bool Contains(T item);
      void CopyTo(T[] array, int arrayIndex);
      int Count { get; }
      bool IsReadOnly { get; }
}

As you can see, in order to extend ICollection<T> in our generic collection, we must implement two properties and five methods. We must also implement two methods which are required by IEnumerator<T> and IEnumerator; the interfaces which ICollection<T> extends to allow foreach enumeration. More on this later. For now, let's define the objects we are going to be collecting.

Example Business Logic Layer

Before we actually write our collection class, we are going to write some foundation code with which it can be used. The example source code attached to this article contains two classes which make up a simple Business Logic Layer that we will use with our generic collection class.

BusinessObjectBase Class

This is the base class from which all of our other business objects will be derived. The class contains only one property called UniqueId (Guid), and a default constructor which initializes that property. Even though the base class we are using here is trivial, much functionality could be added to the base class for a Business Object. As for right now, we are only interested in the way it is derived by other classes because we are going to use this to limit the types that can be used with our generic collection.

C#
//Abstract base class for all business object in the Business Logic Layer
public abstract class BusinessObjectBase
{
    protected Guid? _UniqueId;
    //local member variable which stores the object's UniqueId
   
    //Default constructor
    public BusinessObjectBase()
    {
        //create a new unique id for this business object
        _UniqueId = Guid.NewGuid();
    }

    //UniqueId property for every business object
    public Guid? UniqueId
    {
        get
        {
            return _UniqueId;
        }
        set
        {
            _UniqueId = value;
        }
    }
}

This is nothing too complicated. The base class is abstract to ensure that it must be inherited in order to be instantiated, and it only contains a single property to uniquely identify each business object which it derives.

Person Class

This class will represent a person which we will be storing in our collection. As you can see below, the Person class inherits from the abstract class BusinessObjectBase. This will be important later on when we create our generic collection class.

C#
public class Person : BusinessObjectBase
{
    private string _FirstName = "";
    private string _LastName = "";

    //Paramaterized constructor for immediate instantiation
    public Person(string first, string last)
    {
        _FirstName = first;
        _LastName = last;
    }

    //Default constructor
    public Person()
    {
        //nothing
    }
 
    //Person' First Name 
    public string FirstName 
    {
        get
        {
            return _FirstName;
        } 
        set
        {
            _FirstName = value;
        }
    }

    //Person's Last Name
    public string LastName
    {
        get
        {
            return _LastName;
        }
        set
        {
            _LastName = value;
        }
    }
}

This class is equally trivial, and contains only a FirstName and LastName property, as well as a constructor to initialize those properties immediately, and a default parameterless constructor. Now that we've seen the objects we will be collecting, let's move on to how we will be collecting them.

BusinessObjectCollection<T> Class

This is going to be where most of the magic happens. The methods we implement here from ICollection<T> are going to be responsible for adding and removing objects from our collection, checking the collection for instances of a given object, and instantiating an IEnumerator<T> object in order to enumerate the collection using a foreach statement (we'll look at that later). Below is the definition of BusinessObjectCollection<T>.

C#
using System;
using System.Collections.Generic; 
//needed for non-generic explicit interface
//implementation requirements from ICollection
using System.Collections;
using System.Text;

namespace GenericsExample.Business
{
    public class BusinessObjectCollection<T> : 
           ICollection<T> where T : BusinessObjectBase
    {
        //inner ArrayList object
        protected ArrayList _innerArray;
        //flag for setting collection to read-only
        //mode (not used in this example)
        protected bool _IsReadOnly;

        // Default constructor
        public BusinessObjectCollection()
        {
            _innerArray = new ArrayList();
        }

        // Default accessor for the collection 
        public virtual T this[int index]
        {
            get
            {
                return (T)_innerArray[index];
            }
            set
            {
                _innerArray[index] = value;
            }
        }

        // Number of elements in the collection
        public virtual int Count
        {
            get
            {
                return _innerArray.Count;
            }
        }

        // Flag sets whether or not this collection is read-only
        public virtual bool IsReadOnly
        {
            get
            {
                return _IsReadOnly;
            }
        }

        // Add a business object to the collection
        public virtual void Add(T BusinessObject)
        {
            _innerArray.Add(BusinessObject);
        }

        // Remove first instance of a business object from the collection 
        public virtual bool Remove(T BusinessObject) 
        {
            bool result = false;

            //loop through the inner array's indices
            for (int i = 0; i < _innerArray.Count; i++)
            {
                //store current index being checked
                T obj = (T)_innerArray[i];

                //compare the BusinessObjectBase UniqueId property
                if (obj.UniqueId == BusinessObject.UniqueId)
                {
                    //remove item from inner ArrayList at index i
                    _innerArray.RemoveAt(i);
                    result = true;
                    break;
                }
            }

            return result;
        }

        // Returns true/false based on whether or not it finds
        // the requested object in the collection.
        public bool Contains(T BusinessObject)
        {
            //loop through the inner ArrayList
            foreach (T obj in _innerArray)
            {
                //compare the BusinessObjectBase UniqueId property
                if (obj.UniqueId == BusinessObject.UniqueId)
                {
                    //if it matches return true
                    return true;
                }
            }
            //no match
            return false;
        }
 
        // Copy objects from this collection into another array
        public virtual void CopyTo(T[] BusinessObjectArray, int index)
        {
            throw new Exception(
              "This Method is not valid for this implementation.");
        }

        // Clear the collection of all it's elements
        public virtual void Clear()
        {
            _innerArray.Clear();
        }

        // Returns custom generic enumerator for this BusinessObjectCollection
        public virtual IEnumerator<T> GetEnumerator()
        {
            //return a custom enumerator object instantiated
            //to use this BusinessObjectCollection 
            return new BusinessObjectEnumerator<T>(this);
        }

        // Explicit non-generic interface implementation for IEnumerable
        // extended and required by ICollection (implemented by ICollection<T>)
        IEnumerator IEnumerable.GetEnumerator()
        {
            return new BusinessObjectEnumerator<T>(this);
        }
    }
}

You'll notice the first line of the class declaration:

C#
public class BusinessObjectCollection<T> : 
               ICollection<T> where T : BusinessObjectBase

The syntax above simply tells the compiler to only allow types which are derived from BusinessObjectBase to be allowed in the collection. This is where a generic collection really comes in handy. When we use Generic Type Constraints as above, we are saving ourselves the step of worrying about type-safety errors at runtime. If we try to create a collection of objects which do not derive from BusinessObjectBase, we will get a compiler error. This may sound limiting, but this is exactly what we want in this case, because we may want to add special processing to our collection class that will only work when used on a class that inherits BusinessObjectBase.

You can add other Generic Type Constraints as well, such as the new() constraint, which will tell the compiler that the Generic Type which the collection will store must have a public paramaterless constructor. This is normally used to gain the ability to instantiate an instance of the Generic Type inside the Generic Class. For more information on Generic Type Constraints, visit the MSDN website here.

You'll notice that I have used an ArrayList object as the inner data structure which powers the custom generic collection. Since this is a non-generic type, it could cause some boxing/unboxing if you tried to create a generic collection full of value types (such as integers). But since we are using a constraint to only allow types which are derived from BusinessObjectBase, we don't have to worry about that issue. There will still be some casting down to the object base class done by the ArrayList object, but there is minimal performance degradation, if any at all. You could also use another generic list type as the backing element, such as List<T>, or even a strongly typed array. The performance of using List<T> and ArrayList are pretty much the same, even with a huge (I tested with 20,000 elements) amount of data. The re-dimensioning operations needed to use a strongly typed array, however, are extremely costly, and took up about 70-80% of the total operation time when adding the same number of elements to the collection.

The rest of this class is just a basic implementation of the ICollection<T> interface, with a little bit of custom logic for the Remove() and Contains() methods, which look at the UniqueId field of the BusinessObjectBase class in order to determine an object's existence within the collection.

There is, however, a bit of code worth looking at which ties in with our next class:

C#
 // Returns custom generic enumerator for this BusinessObjectCollection
public virtual IEnumerator<T> GetEnumerator()
{
   //return a custom enumerator object
   //instantiated to use this BusinessObjectCollection 
   return new BusinessObjectEnumerator<T>(this);
}

// Explicit non-generic interface implementation for IEnumerable
// extended and required by ICollection (implemented by ICollection<T>)
IEnumerator IEnumerable.GetEnumerator()
{
   return new BusinessObjectEnumerator<T>(this);
}

These two implementations are what the foreach loop will be calling in order to loop through our custom collection. We have to define a custom generic Enumerator object that will perform the actual looping. If you've never dealt with generic interface implementations before, you may be wondering why there are two implementations of GetEnumerator() here. The second method is an explicit non-generic interface implementation for IEnumerable. Why do we have to do this? The answer is simple, because IEnumerator<T> implements the IEnumerable interface, which is non-generic. And, since we are implementing ICollection<T>, we must account for its non-generic roots.

Now that we know how we will be collecting the Business Objects, let's move on to how we will be retrieving them using a custom generic Enumerator.

BusinessObjectEnumerator<T> Class

Below is the code definition for our custom generic Enumerator which will be used to enumerate through instances of the BusinessObjectCollection<T> class. The code is very simple to understand, with a few main points of interest on which I will touch below.

C#
 public class BusinessObjectEnumerator<T> : IEnumerator<T> where T : BusinessObjectBase 
{
    protected BusinessObjectCollection<T> _collection; //enumerated collection
    protected int index; //current index
    protected T _current; //current enumerated object in the collection

    // Default constructor
    public BusinessObjectEnumerator()
    {
        //nothing
    }

    // Paramaterized constructor which takes
    // the collection which this enumerator will enumerate
    public BusinessObjectEnumerator(BusinessObjectCollection<T> collection)
    {
        _collection = collection;
        index = -1;
        _current = default(T);
    }

    // Current Enumerated object in the inner collection
    public virtual T Current
    {
        get
        {
            return _current;
        }
    }

    // Explicit non-generic interface implementation for IEnumerator
    // (extended and required by IEnumerator<T>)
    object IEnumerator.Current
    {
        get
        {
            return _current;
        }
    }

    // Dispose method
    public virtual void Dispose()
    {
        _collection = null;
        _current = default(T);
        index = -1;
    }

    // Move to next element in the inner collection
    public virtual bool MoveNext()
    {
        //make sure we are within the bounds of the collection
        if (++index >= _collection.Count)
        {
            //if not return false
            return false;
        }
        else
        {
             //if we are, then set the current element
             //to the next object in the collection
            _current = _collection[index];
        }
        //return true
        return true;
    }

    // Reset the enumerator
    public virtual void Reset()
    {
        _current = default(T); //reset current object
        index = -1;
    }
}

First, you should take note of the first line of this class declaration:

C#
public class BusinessObjectEnumerator<T> : IEnumerator<T> where T : BusinessObjectBase 

As you can see here, we have implemented the same generic constraints in order to ensure the type of objects we will be enumerating through are derived from BusinessObjectBase.

We also define three member variables which we will use to control the flow of the enumeration. They are:

C#
protected BusinessObjectCollection<T> _collection; //enumerated collection
protected int index; //current index
protected T _current; //current enumerated object in the collection

The first is the internal instance of BusinessObjectCollection which is being traversed. This variable gets set through the parameterized constructor inside the BusinessObjectEnumerator<T> class. The BusinessObjectEnumerator itself gets instantiated once a foreach loop calls the GetEnumerator() method of the BusinessObjectCollection class. Each time an item is found, the foreach loop moves to the next item in the collection using the MoveNext() method found above.

The other two member variables are simply a reference to the current object of type T being read and an index to let us know where we are at in the collection. The rest of the IEnumerator<T> implementation is pretty simple to understand, so I won't waste your time or strain your eyes by explaining it all here.

Now that you've defined all your classes and made them work with each other, the only thing left to do is write a little "driver" or example program to demonstrate the Custom Generic Collection's functionality. I included a sample console application which demonstrates the usage of the generic classes explained here, with the source code for the classes themselves. I also added in another little console application I wrote in order to test the amount of time it took to complete different collection based operations using different generic and non-generic types, with boxing/unboxing and casting.

Conclusion

Custom Generic Collections can be a huge advantage for your application, and save you a lot of time and trouble. By creating a type-safe environment in which your objects will be collected, you initially rid yourself of having to worry about what actually gets stored in the collection at run-time. You also gain the ability to reuse and extend your code any way you wish by not hard-coding data types. You could easily remove the constraints in the classes above to create a solution that fits your exact needs and could be extended to fit your needs in the future as well.

You may be thinking, "Well, all this is great, but why not just use List<T> and be done with it?" Well, you could! There are many different ways to implement type-safe and polymorphic data structures in C#. This is but one example. I hope this article has been some help to someone out there, and I look forward to getting feedback from the community.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)