Handling collections elegantly in C#
Interfaces vs Concrete types
27 July 2008
Shed a tear for the humble array that has been largely abstracted to a realm of computing where only the brave
or stupid dare tread anymore.
The .NET framework offers a plethora of choice for your "collections".
In fact, there are so many collections provided in the framework that I keep on stumbling upon collection classes I have
never seen before.
Here is a sample of some of the collections available in the framework:
List of C# .NET collections
- Normal array (i.e. string[])
- Array
- ArrayList
- BindingList
- BitArray
- Collection
- DataTable
- DataView
- Dictionary
- HashSet
- HashTable
- HybridDictionary
- KeyedCollection
- KeyedByTypeCollection
- LinkedList
- List
- ListDictionary
- NameValueCollection
- ObservableCollection
- OrderedDictionary
- Queue
- Queue (generic)
- ReadOnlyCollection
- ReadOnlyObservableCollection
- SortedList
- SortedList (generic)
- Stack
- Stack (generic)
- StringCollection
- StringDictionary
- SynchronizedCollection
- SynchronizedKeyedCollection
- SynchronizedReadOnlyCollection
There are plenty more classes buried within the framework that derive from these collections and are usually closely coupled with other
classes. It sometimes seems that every, single class requires a corresponding Collection class - so we have
AttributeCollection,
ComponentCollection, HttpCookieCollection,
ListSortDescriptionCollection etc.
Funnily enough, there are a tonne of collections, common amongst other programming languages, that are missing from the .NET framework too. If you really want to get crazy with
collections, try out the NGenerics
framework, which at the time of writing includes:
- Bag
- Graph
- HashList
- Heap
- ObjectMatrix
- PascalSet
- CircularQueue
- Vector2D
- Vector3D
- VectorN
- PriorityQueue
- SkipList
- BinaryTree
- BinarySearchTree
- GeneralTree
- RedBlackTree
- SplayTree
Which collection should I use?
The majority of programmers out there probably never use more than a handful of the available collections, unless they are
working in specialized fields or are particularly adventurous. What happen instead, is that programmers develop an affinity
with their favourite programming patterns and do things the same way every time.
The trusty and powerful List becomes ubiquitous and one rarely needs any further functionality,
so why bother with all those other fancy classes?
The trouble with Collections
But what happens when you need to work with a framework that prefers different types of collections to all your code?
I recently had a bad experience upgrading my version of the SubSonic framework (a data access layer framework).
Originally all their collections inherited from the generic List. But for some reason they decided to switch
to inheriting from BindingList.
Ouch! Throughout my code I had been relying on the methods available within the List class, such as
sorting, finding by a predicate etc. After upgrading SubSonic and compiling I was greeted with a list of more than 100 errors. This
couldn't be fixed with a simple global find and replace either... this required severe old-school finger pumping.
Coding to interfaces... a solution?
At some point in a programmers career, there occurs a shift in his or her output, from code that never uses interfaces to
code that use interfaces everywhere. Some people never reach this point, but for those who do, truly understanding
interfaces occurs as a sort of epiphany - opening up a whole new world.
Evidence of such a shift can be uncovered in the .NET framework itself - with each successive iteration of the framework,
interfaces become more prevalant and more useful.
If you are still skeptical as to the benefits of interfaces, I highly recommend reading up on design
patterns in general. The Head First book is a great place to start.
The advantage of coding to interfaces starts to become clear in the case of my troubles with SubSonic and its collections.
Instead of all my methods requesting concrete classes, such as Lists, as their parameters, I should have
had them request interfaces, such as the ICollection.
public void DoSomething(ICollection<Foo> collection)
{
// Lend me your ICollections
}
Because the majority of the collections in the .NET framework inherit from ICollection, or at the least IEnumerable, theoretically
life is now easier. If the instantiated concrete classes change, as long as those classes still implement ICollection,
everything is peachy. Even if the classes don't implement ICollection, a variety of sneaky design patterns
can be wielded to brutally solve the problem - Adapters, Decorators and so forth.
Limitations of IList
Unfortunately, certain limitations of C# and .NET framework mean that coding to interfaces is not always an optimal solution.
No Covariance
Covariance of generic collections is not supported in C#. Bummer.
Imagine I have a list of strings (List<string>). One would think that I could pass my string list
to a method's parameter that required a list of objects (List<object>), without having to engage
adapters or helpers or anything else. Unfortunately a clean pass is impossible.
/* This will never happen */
public void LegalizeMarijuana()
{
List<MarijuanaStrain> strains = LoadStrains();
Legalize(strains);
}
public List<LegalDrug> Legalize(IList<IllegalDrug> drugs)
{
// etc
}
A variety of adapter classes can be found out on the net to help with this, but inevitably you end up with more bloated code:
public void LegalizeMarijuana()
{
List<MarijuanaStrain> strains = LoadStrains();
/* Adapt to a list of illegal drugs */
Legalize(new ListAdapter<MarijuanaStrain,IllegalDrug>(strains));
}
public List<LegalDrug> Legalize(IList<IllegalDrug> drugs)
{
// etc
}
Despite the bloat, it is lucky that the Legalize method accepted
an IList rather than a concrete List, or else we would struggle to use
the adapter class at all.
Here are a set of adapter classes for you to download
(credits go to the Umbrella framework).
Update: Apparently C# 4.0 will be supporting covariance and contravariance - bring on 2010!
Not all Lists are ILists
Some of the collections in the .NET framework don't implement all the interfaces you would expect them to.
Thankfully the ICollection has been used fairly thoroughly, so we can at least code to
ICollection, even if IList is not
implemented.
But I like my List!
The next problem, is that there is a whole bunch of useful functionality available in the List class.
If you start accepting ILists everywhere, you are going to find yourself unable to sort, find,
remove a range etc. This is a real pity, because these are all very convenient methods and save hours of coding.
If you're sticking with ICollection then you are in for even more tears because it doesn't even
have methods for adding, indexing or removing!
As far as I know, there are two options here to try and regain some lost ground:
- Use static helper classes (much better with C# 3's extension methods)
- Roll your own IFancyList interface(s), then combine it with decorator classes
that extended ICollections with sorting, searching etc.
The first option, creating some extension methods, is probably easiest and will get you out of many sticky situations.
Conclusion
Despite the problems, I still think that it is wise to accept interfaces rather than concrete classes.
With careful use of adapters and C# 3 extension methods, the situation is not too bad.
If some hard and fast rules were needed to add to your company policy and enforce upon your underlings, perhaps something along
these lines would do the trick:
- For all public properties and method parameters, accept the least restrictive interface
- For all custom collections, implement the most restrictive interface possible
This means that if a custom collection is in the slightest bit list-like, it should implement IList
whenever possible. Conversly, all your methods should accept ICollections, even if this means
re-implementing some functionality through static extension classes or decorators.