The System.Collections.Generic.Dictionary<TKey, TValue> class is one of the most useful of all .NET collections. It maps a key to a value, and allows for fast retrieval based on the key, as it’s implemented as a hash table, calling GetHashCode on the key object to get to a specific “bucket”, and then looks up the actual value (with Object.Equals or a specific IEqualityComparer<Tkey>.Equals).
One feature that Dictionary<> doesn’t support is the ability to access items by integer index. That is, insertion order is not maintained. For most cases, this may be ok, but some cases require fast search and index based access.
Some values contain the keys themselves. Here’s a simple Customer class:
Let’s suppose that the Id of a Customer is unique, and so can serve as a key. Here’s a dictionary of such customers:
This seems harmless enough, but notice that the key (Id) is supplied twice. This may not seem like a big deal, but the key could be a more complex entity. It’s also not as elegant as we would like. I’d rather just use code like the following:
And get the same result, that is, the Id is still a key to search by. Dictionary<> cannot take that syntax. Enter KeyedCollection<>.
KeyedCollection<TKey, TValue> is an abstract base class that inherits from Collection<TValue>, thus giving it index-based access. A requirement is that the key is somewhere inside the value, or connected to the value, as is the case with our Customer class. The only question remains is how to return a key based on a value?
This is why we need a new class, extending KeyedCollection<> and overriding a single abstract method, GetKeyForItem:
This says that given a Customer, its key is its Id. Now we can use this class as follows:
We just add Customer objects, and internally they are keyed by Id. The Add method used is the one from Collection<TValue>. This means, we can also access customers by index, or search using the Contains method.
As an extra bonus, the KeyedCollection<> can be configured to not create an internal Dictionary<>, depending on the number of items. For a small number of items, Dictionary<> is an overkill, and a linear search is faster. By default, a Dictionary is created upon first insertion, but this can be configured by using one of KeyedCollection<>’s constructors.
The idea of mapping a value to a key as was done in GetKeyForItem can be generalized, so that we don’t have to create new classes for new types. Here’s a generalized class that uses a delegate to execute the required code in GetKeyForItem:
With this class in hand, we can create a Customer collection like so: