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.
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.
public abstract class BusinessObjectBase
{
protected Guid? _UniqueId;
public BusinessObjectBase()
{
_UniqueId = Guid.NewGuid();
}
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.
public class Person : BusinessObjectBase
{
private string _FirstName = "";
private string _LastName = "";
public Person(string first, string last)
{
_FirstName = first;
_LastName = last;
}
public Person()
{
}
public string FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
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>
.
using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
namespace GenericsExample.Business
{
public class BusinessObjectCollection<T> :
ICollection<T> where T : BusinessObjectBase
{
protected ArrayList _innerArray;
protected bool _IsReadOnly;
public BusinessObjectCollection()
{
_innerArray = new ArrayList();
}
public virtual T this[int index]
{
get
{
return (T)_innerArray[index];
}
set
{
_innerArray[index] = value;
}
}
public virtual int Count
{
get
{
return _innerArray.Count;
}
}
public virtual bool IsReadOnly
{
get
{
return _IsReadOnly;
}
}
public virtual void Add(T BusinessObject)
{
_innerArray.Add(BusinessObject);
}
public virtual bool Remove(T BusinessObject)
{
bool result = false;
for (int i = 0; i < _innerArray.Count; i++)
{
T obj = (T)_innerArray[i];
if (obj.UniqueId == BusinessObject.UniqueId)
{
_innerArray.RemoveAt(i);
result = true;
break;
}
}
return result;
}
public bool Contains(T BusinessObject)
{
foreach (T obj in _innerArray)
{
if (obj.UniqueId == BusinessObject.UniqueId)
{
return true;
}
}
return false;
}
public virtual void CopyTo(T[] BusinessObjectArray, int index)
{
throw new Exception(
"This Method is not valid for this implementation.");
}
public virtual void Clear()
{
_innerArray.Clear();
}
public virtual IEnumerator<T> GetEnumerator()
{
return new BusinessObjectEnumerator<T>(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new BusinessObjectEnumerator<T>(this);
}
}
}
You'll notice the first line of the class declaration:
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:
public virtual IEnumerator<T> GetEnumerator()
{
return new BusinessObjectEnumerator<T>(this);
}
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.
public class BusinessObjectEnumerator<T> : IEnumerator<T> where T : BusinessObjectBase
{
protected BusinessObjectCollection<T> _collection;
protected int index;
protected T _current;
public BusinessObjectEnumerator()
{
}
public BusinessObjectEnumerator(BusinessObjectCollection<T> collection)
{
_collection = collection;
index = -1;
_current = default(T);
}
public virtual T Current
{
get
{
return _current;
}
}
object IEnumerator.Current
{
get
{
return _current;
}
}
public virtual void Dispose()
{
_collection = null;
_current = default(T);
index = -1;
}
public virtual bool MoveNext()
{
if (++index >= _collection.Count)
{
return false;
}
else
{
_current = _collection[index];
}
return true;
}
public virtual void Reset()
{
_current = default(T);
index = -1;
}
}
First, you should take note of the first line of this class declaration:
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:
protected BusinessObjectCollection<T> _collection;
protected int index;
protected T _current;
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