![]() |
Languages »
C# »
General
Intermediate
License: The Code Project Open License (CPOL)
Implementing C# Generic Collections using ICollection<T>By Jake WeakleyAn article explaining one way to implement a generic collection in C# using ICollection<T> with an example Business Logic Layer |
VC7, VC8.0, C# 2.0, C# 3.0.NET 2.0, WinXP, Win2003, Vista, .NET 3.0, ASP.NET, WebForms, VS2005, VS2008, Dev
|
||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

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.
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 operation 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.
One of the main differences between C++ Template Classes and C# Generics is that in C# you cannot use any 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.
Before we get started lets 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 2 properties and 5 methods. We must also implement 2 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, lets define the objects we are going to be collecting.
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 2 classes which make up a simple Business Logic Layer that we will use with our generic collection class.
This is the base class from which all of our other business objects will be derived. The class contains only 1 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.
//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.
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 = "";
//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, lets move on to how we will be collecting them.
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; //needed for non-generic explicit interface implementation requirements from ICollection
using System.Text;
namespace GenericsExample.Business
{
public class BusinessObjectCollection<T> : ICollection<T> where T : BusinessObjectBase
{
protected ArrayList _innerArray; //inner ArrayList object
protected bool _IsReadOnly; //flag for setting collection to read-only mode (not used in this example)
// 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:
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 into 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 object 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 one 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 web site 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 a minimal performance degredation, 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 redimensioning operations needed to use a strongly typed array, however, are extremely costly and took up about %70-80 of total operation time when adding the same number of elements to the collection.
The rest of this class is just 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:
// 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 implementation before you may be wondering why there are 2 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 implementing ICollection<T> we must account for it's non-generic roots.
Now that we now how we will be collecting the Business Objects, lets move on to how we will be retrieving them using a custom generic Enumerator.
Below if 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; //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 beginning line of this class declation:
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 3 member variables which we will use to control the flow of the enumeration. They are:
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 the 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 2 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.
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.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 8 Nov 2007 Editor: |
Copyright 2007 by Jake Weakley Everything else Copyright © CodeProject, 1999-2009 Web17 | Advertise on the Code Project |