Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C#
Article

Generic Keyed List

Rate me:
Please Sign up or sign in to vote.
4.82/5 (20 votes)
27 Jan 20063 min read 175.8K   925   62   37
A KeyedList using C# 2.0 Generics.

Introduction

A while ago, I wrote a KeyedList for C# 1.0 (or is it 1.1?). Recently I needed the KeyedList for a C# 2.0 project, and so it occurred to me to rewrite it, but this time using generics.

In brief, a keyed list is an ordered dictionary. Unlike the Dictionary<TKey, TValue> provided by .NET 2.0, which does not guarantee order, the KeyedList<TKey, TValue> is a dictionary that also implements ordinal indexing in addition to keyed indexing. This is useful when you are interested in indexing by ordinal value rather than key.

Implemented Interfaces

The KeyedList implements the IDictionary<TKey, TValue> interface and its properties and methods:

  • Item
  • Keys
  • Values
  • ContainsKey
  • Add
  • Remove
  • TryGetValue

and the IList<TKey> interface with its properties and methods:

  • Item
  • IndexOf
  • Insert
  • RemoveAt

Implementation

Internally, the KeyedList class maintains both a dictionary and a list:

C#
private Dictionary<K, V> objectTable = new Dictionary<K, V>();
private List<KeyValuePair<K, V>> objectList = new List<KeyValuePair<K, V>>();

As per the MSDN documentation for Dictionary<TKey, TValue>: "The order of the keys in the Dictionary.KeyCollection is unspecified". A second collection, a List<KeyValuePair<T, V>>, is used to maintain the ordering of the key-value pairs. An important distinction is therefore made between:

C#
/// <summary>
/// Get an unordered list of keys.
/// This collection refers back to the keys in the original Dictionary.
/// </summary>
public ICollection<K> Keys
{
  get { return objectTable.Keys; }
}

/// <summary>
/// Get an unordered list of values.
/// This collection refers back to the values in the original Dictionary.
/// </summary>
public ICollection<V> Values
{
  get { return objectTable.Values; }
}

and:

C#
/// <summary>
/// Get the ordered list of keys.
/// This is a copy of the keys in the original Dictionary.
/// </summary>
public List<K> OrderedKeys
{
  get
  {
    List<K> retList = new List<K>();

    foreach (KeyValuePair<K, V> kvp in objectList)
    {
      retList.Add(kvp.Key);
    }

    return retList;
  }
}

/// <summary>
/// Get the ordered list of values.
/// This is a copy of the values in the original Dictionary.
/// </summary>
public List<V> OrderedValues
{
  get
  {
    List<V> retList = new List<V>();

    foreach (KeyValuePair<K, V> kvp in objectList)
    {
      retList.Add(kvp.Value);
    }

    return retList;
  }
}

The Values and Keys properties return a collection that is not a copy, so changes to the dictionary continue to be reflected in the key collection. However, this is not true for OrderedValues and OrderedKeys properties, as this is a local copy.

Indexers

Two indexers are implemented--ordinal and key:

C#
/// <summary>
/// Get/Set the value at the specified index.
/// </summary>
/// <param name="idx">The index.</param>
/// <returns>The value.</returns>
public KeyValuePair<K, V> this[int idx]
{
  get
  {
    if (idx < 0 || idx >= Count)
    {
      throw new ArgumentOutOfRangeException("index");
    }

    return objectList[idx]; 
  }
  set
  {
    if (idx < 0 || idx >= Count)
    {
      throw new ArgumentOutOfRangeException("index");
    }

    objectList[idx] = value;
    objectTable[value.Key] = value.Value;
  }
}

/// <summary>
/// Get/Set the value associated with the specified key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The associated value.</returns>
public virtual V this[K key]
{
  get { return objectTable[key]; }
  set
  {
    if (objectTable.ContainsKey(key))
    {
      objectTable[key] = value;
      objectList[IndexOf(key)] = new KeyValuePair<K, V>(key, value);
    }
    else
    {
      Add(key, value);
    }
  }
}

The index by key setter introduces a complexity--what if the key doesn't exist? In the dictionary, a key-value pair is created, but in the KeyedList, where does this entry get created in the ordered list? The behavior that I've chosen is to simply append the new key-value pair to the end of the list, however you may prefer a different implementation, perhaps even throwing an exception, so this method has been marked virtual.

Also note that, the ordinal indexer returns a KeyValuePair, whereas the key indexer returns the value. So it would be useful to also have methods that, given the ordinal index, simply returns the key or the value:

C#
/// <summary>
/// Returns the key at the specified index.
/// </summary>
/// <param name="idx">The index.</param>
/// <returns>The key at the index.</returns>
public K GetKey(int idx)
{
  if (idx < 0 || idx >= Count)
  {
    throw new ArgumentOutOfRangeException("index");
  }

  return objectList[idx].Key;
}

/// <summary>
/// Returns the value at the specified index.
/// </summary>
/// <param name="idx">The index.</param>
/// <returns>The value at the index.</returns>
public V GetValue(int idx)
{
  if (idx < 0 || idx >= Count)
  {
    throw new ArgumentOutOfRangeException("index");
  }

  return objectList[idx].Value;
}

IndexOf

We also may want to acquire the index, given the key, but IList<KeyValuePair<K, V>> requires that IndexOf(KeyValuePair<K, V>>) be implemented. This results in a slightly odd method because, after all, the key is unique, so there can't be different values for the same key. This method simply calls the IndexOf(K key) method. For large collections, it probably would be a considerable performance improvement if the dictionary wrapped the index value in an internal "Value" class, so you could use the dictionary's hash lookup to acquire the index value. However, that's beyond the scope of this implementation, which is simply:

C#
/// <summary>
/// Get the index of a particular key.
/// </summary>
/// <param name="key">The key to find the index of.</param>
/// <returns>The index of the key, or -1 if not found.</returns>
public int IndexOf(K key)
{
  int ret = -1;

  for (int i = 0; i < objectList.Count; i++)
  {
    if (objectList[i].Key.Equals(key))
    {
      ret = i;
      break;
    }
  }

  return ret;
}

/// <summary>
/// Given the key-value pair, find the index.
/// </summary>
/// <param name="kvp">The key-value pair.</param>
/// <returns>The index, or -1 if not found.</returns>
public int IndexOf(KeyValuePair<K, V> kvp)
{
  return IndexOf(kvp.Key);
}

Contains

We need to implement a Contains method for both the IDictionary and the IList interfaces (this theme is repeated for Add and Remove as well). Again, only the key is tested when the KeyValuePair<K, V> is passed:

C#
/// <summary>
/// Test if the KeyedList contains the key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>True if the key is found.</returns>
public bool ContainsKey(K key)
{
  return objectTable.ContainsKey(key);
}

/// <summary>
/// Test if the KeyedList contains the key in the key-value pair.
/// </summary>
/// <param name="kvp">The key-value pair.</param>
/// <returns>True if the key is found.</returns>
public bool Contains(KeyValuePair<K, V> kvp)
{
  return objectTable.ContainsKey(kvp.Key);
}

Add

The Add methods add an entry to the dictionary and a key-value pair to the list:

C#
/// <summary>
/// Adds a key-value pair to the KeyedList.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The associated value.</param>
public void Add(K key, V value)
{
  objectTable.Add(key, value);
  objectList.Add(new KeyValuePair<K, V>(key, value));
}

/// <summary>
/// Adds a key-value pair to the KeyedList.
/// </summary>
/// <param name="kvp">The KeyValuePair instance.</param>
public void Add(KeyValuePair<K, V> kvp)
{
  Add(kvp.Key, kvp.Value);
}

Insert

Insert is similar to add, except that it inserts the key and value at the specified location in the list:

C#
/// <summary>
/// Insert the key-value at the specified index.
/// </summary>
/// <param name="idx">The zero-based insert point.</param>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
public void Insert(int idx, K key, V value)
{
  if ( (idx < 0) || (idx > Count) )
  {
    throw new ArgumentOutOfRangeException("index");
  }

  objectTable.Add(key, value);
  objectList.Insert(idx, new KeyValuePair<K, V>(key, value));
}

/// <summary>
/// Insert the key-value pair at the specified index location.
/// </summary>
/// <param name="idx">The key.</param>
/// <param name="kvp">The value.</param>
public void Insert(int idx, KeyValuePair<K, V> kvp)
{
  if ( (idx < 0) || (idx > Count) )
  {
    throw new ArgumentOutOfRangeException("index");
  }

  objectTable.Add(kvp.Key, kvp.Value);
  objectList.Insert(idx, kvp);
}

Remove

Remove lets you remove an entry either by key, key-value pair, or index:

C#
/// <summary>
/// Remove the entry.
/// </summary>
/// <param name="key">The key identifying the key-value pair.</param>
/// <returns>True if removed.</returns>
public bool Remove(K key)
{
  bool found=objectTable.Remove(key);

  if (found)
  {
    objectList.RemoveAt(IndexOf(key));
  }

  return found;
}

/// <summary>
/// Remove the key in the specified KeyValuePair instance. The Value
/// property is ignored.
/// </summary>
/// <param name="kvp">The key-value identifying the entry.</param>
/// <returns>True if removed.</returns>
public bool Remove(KeyValuePair<K, V> kvp)
{
  return Remove(kvp.Key);
}

/// <summary>
/// Remove the entry at the specified index.
/// </summary>
/// <param name="idx">The index to the entry to be removed.</param>
public void RemoveAt(int idx)
{
  if ( (idx < 0) || (idx >= Count) )
  {
    throw new ArgumentOutOfRangeException("index");
  }

  objectTable.Remove(objectList[idx].Key);
  objectList.RemoveAt(idx);
}

GetEnumerator

The interfaces require two GetEnumerator methods:

C#
/// <summary>
/// Returns an ordered System.Collections KeyValuePair objects.
/// </summary>
IEnumerator IEnumerable.GetEnumerator()
{
  return objectList.GetEnumerator();
}

/// <summary>
/// Returns an ordered KeyValuePair enumerator.
/// </summary>
IEnumerator<KeyValuePair<K, V>> 
    IEnumerable<KeyValuePair<K, V>>.GetEnumerator()
{
  return objectList.GetEnumerator();
}

Note that this test calls the IDictionary<TKey, TValue> enumerator.

C#
[Test]
public void EnumerationTest()
{
  int n = 0;

  foreach (KeyValuePair<string, int> kvp in k)
  {
    Assertion.Assert(kvp.Value == n, "Expected ordered list of values.");
    ++n;
  }
}

In fact, you have to specifically cast to get the non-generic enumerator.

Miscellaneous

A few other methods are implemented as well:

C#
/// <summary>
/// Returns false.
/// </summary>
public bool IsReadOnly
{
  get { return false; }
}

/// <summary>
/// Returns the number of entries in the KeyedList.
/// </summary>
public int Count
{
  get { return objectList.Count; }
}

/// <summary>
/// Attempt to get the value, given the key, without throwing an exception 
/// if not found.
/// </summary>
/// <param name="key">The key indentifying the entry.</param>
/// <param name="val">The value, if found.</param>
/// <returns>True if found.</returns>
public bool TryGetValue(K key, out V val)
{
  return objectTable.TryGetValue(key, out val);
}

/// <summary>
/// Copy the entire key-value pairs to the KeyValuePair array, starting
/// at the specified index of the target array. The array is populated 
/// as an ordered list.
/// </summary>
/// <param name="kvpa">The KeyValuePair array.</param>
/// <param name="idx">The position to start the copy.</param>
public void CopyTo(KeyValuePair<K, V>[] kvpa, int idx)
{
  objectList.CopyTo(kvpa, idx);
}

Unit Tests

The download includes unit tests for each of the properties/methods in this class. Hopefully it has been tested sufficiently!

Conclusion

It's not often that one needs a KeyedList, but it is unfortunate that .NET 2.0 doesn't provide one. So hopefully this implementation will meet your needs.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralRe: Bug!?! Pin
Marc Clifton24-May-06 8:42
mvaMarc Clifton24-May-06 8:42 
GeneralRe: Bug!?! Pin
Marc Clifton24-May-06 8:40
mvaMarc Clifton24-May-06 8:40 
GeneralVts.UnitTest Pin
Doncp1-Apr-06 6:06
Doncp1-Apr-06 6:06 
QuestionWhat about KeyedCollection? Pin
ekriel1-Feb-06 6:18
ekriel1-Feb-06 6:18 
GeneralSuggestion Pin
jmw28-Jan-06 5:27
jmw28-Jan-06 5:27 
QuestionIndexer Pin
Steve Hansen28-Jan-06 3:11
Steve Hansen28-Jan-06 3:11 
AnswerRe: Indexer Pin
Marc Clifton28-Jan-06 11:56
mvaMarc Clifton28-Jan-06 11:56 
QuestionRe: Indexer Pin
Leonardo Pessoa30-Jan-06 0:00
Leonardo Pessoa30-Jan-06 0:00 
Now, this is a great question to me: how does the compiler behave on this situation? Does it compile or report an error?

[]'s,
Harkos
---
"Money isn't our god, integrity will free our soul."
Cut Throat - Sepultura
AnswerRe: Indexer Pin
Richard Deeming30-Jan-06 7:57
mveRichard Deeming30-Jan-06 7:57 
GeneralRe: Indexer Pin
Leonardo Pessoa31-Jan-06 1:18
Leonardo Pessoa31-Jan-06 1:18 
AnswerRe: Indexer Pin
Drew Noakes5-Jun-07 3:52
Drew Noakes5-Jun-07 3:52 
QuestionMemory? Pin
Matthew Hazlett27-Jan-06 14:20
Matthew Hazlett27-Jan-06 14:20 
AnswerRe: Memory? Pin
Marc Clifton27-Jan-06 15:44
mvaMarc Clifton27-Jan-06 15:44 
GeneralRe: Memory? Pin
S. Senthil Kumar28-Jan-06 20:08
S. Senthil Kumar28-Jan-06 20:08 
GeneralRe: Memory? Pin
Leonardo Pessoa29-Jan-06 23:58
Leonardo Pessoa29-Jan-06 23:58 
GeneralRe: Memory? Pin
S. Senthil Kumar30-Jan-06 6:15
S. Senthil Kumar30-Jan-06 6:15 
GeneralRe: Memory? Pin
Leonardo Pessoa31-Jan-06 1:29
Leonardo Pessoa31-Jan-06 1:29 
GeneralRe: Memory? Pin
S. Senthil Kumar31-Jan-06 5:57
S. Senthil Kumar31-Jan-06 5:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.