Updated on 2020-02-24
A relatively safe, simple, yet high performance technique for using lists as dictionary keys.
When doing functional style programming in C#, you'll probably eventually run into a situation where you need to key a dictionary by an ordered list of items. There are good ways and not so good ways to do this. This article aims to present you with an efficient and relatively safe way to use lists as dictionary keys.
Dictionary keys must be comparable for equality, must present a proper hash code, and must be immutable. These requirements make it a little bit involved to use lists as dictionary keys since lists don't typically provide value semantics - in this case, item by item comparison and a proper hash code. Perhaps worse, lists allow you to modify them at any time.
In summary, there are two major issues here, and one is comparing lists item by item isn't very efficient, and using a standard IList
We can solve the latter more or less by requiring the use of IReadOnlyList
The former requires a little more work. We have to implement value equality semantics on our KeyList
sealed class KeyList<T> : IReadOnlyList<T>, IEquatable<KeyList<T>>
{
int _hashCode;
List<T> _items;
public KeyList()
{
_items = new List<T>();
_hashCode = 0;
}
public KeyList(int capacity)
{
_items = new List<T>(capacity);
_hashCode = 0;
}
public T this[int index] => _items[index];
public int Count => _items.Count;
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(T item)
{
_items.Add(item);
if (null != item)
_hashCode ^= item.GetHashCode();
}
public override int GetHashCode()
{
return _hashCode;
}
public bool Equals(KeyList<T> rhs)
{
if (ReferenceEquals(this, rhs))
return true;
if (ReferenceEquals(rhs, null))
return false;
if (rhs._hashCode != _hashCode)
return false;
var ic = _items.Count;
if (ic != rhs._items.Count)
return false;
for(var i = 0;i<ic;++i)
if (!Equals(_items[i], rhs._items[i]))
return false;
return true;
}
public override bool Equals(object obj)
{
return Equals(obj as KeyList<T>);
}
}
The important methods here are Add() and the main Equals() method. You can see in Add() we're modifying the _hashCode field for each item added. You can see in the Equals() method we short circuit if the hash codes are not equal, before finally doing an item by item comparison. This can vastly improve performance. The other performance gain is in the GetHashCode() method which doesn't have to compute anything. Since these methods are typically called by the IDictionary<TKey,TValue> implementation, we want them to be as quick as possible, especially the GetHashCode() method which is called a lot by Dictionary<TKey, TValue>.
If you run the code in Release mode, you'll get accurate performance metrics. In Debug not so much, so the program will warn you if you're running in Debug. You can see we work the dictionaries, one filled with standard lists, the other filled with our special KeyList
It has been pointed out that this is not correct if the T type is mutable. This is true. However, the same is true of Dictionary<TKey, TValue> and HashSet
It is possible to adapt this technique to other collections if you need to. You can adapt it to a HashSet