Description
EDITED on 4/10/2024 by @stephentoub to update proposal
Often times I've come across places when needing a Dictionary
where the insertion order of the elements is important to me. Unfortunately, .NET does not currently have a generic OrderedDictionary
class. We've had a non-generic OrderedDictionary
class since .NET Framework 2.0 which oddly enough was when generics were added but no generic equivalent. This has forced many to roll their own solution, typically by using a combination of a List
and Dictionary
field resulting in the worst of both worlds in terms of performance and resulting in larger memory usage, and even worse sometimes users instead rely on implementation details of Dictionary
for ordering which is quite dangerous.
Proposed API
namespace System.Collections.Generic;
public class OrderedDictionary<TKey, TValue> :
IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary,
IList<KeyValuePair<TKey, TValue>>, IReadOnlyList<KeyValuePair<TKey, TValue>>, IList
where TKey : not null
{
public OrderedDictionary();
public OrderedDictionary(int capacity);
public OrderedDictionary(IEqualityComparer<TKey>? comparer);
public OrderedDictionary(int capacity, IEqualityComparer<TKey>? comparer);
public OrderedDictionary(IDictionary<TKey, TValue> dictionary);
public OrderedDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey>? comparer);
public OrderedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection);
public OrderedDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey>? comparer);
public IEqualityComparer<TKey> Comparer { get; }
public OrderedDictionary<TKey, TValue>.KeyCollection Keys { get; }
public OrderedDictionary<TKey, TValue>.ValueCollection Values { get; }
public int Count { get; }
public TValue this[TKey key] { get; set; }
public void Add(TKey key, TValue value);
public void Clear();
public bool ContainsKey(TKey key);
public bool ContainsValue(TValue value);
public KeyValuePair<TKey, TValue> GetAt(int index);
public OrderedDictionary<TKey, TValue>.Enumerator GetEnumerator();
public int IndexOf(TKey key);
public void Insert(int index, TKey key, TValue value);
public bool Remove(TKey key);
public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value);
public void RemoveAt(int index);
public void SetAt(int index, TValue value);
public void SetAt(int index, TKey key, TValue value);
public void TrimExcess();
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
public KeyValuePair<TKey, TValue> Current { get; }
public void Dispose();
public bool MoveNext();
}
public sealed class KeyCollection : IList<TKey>, IReadOnlyList<TKey>, IList
{
public int Count { get; }
public bool Contains(TKey key);
public void CopyTo(TKey[] array, int arrayIndex);
public OrderedDictionary<TKey, TValue>.KeyCollection.Enumerator GetEnumerator();
public struct Enumerator : IEnumerator<TKey>
{
public TKey Current { get; }
public bool MoveNext();
public void Dispose();
}
}
public sealed class ValueCollection : IList<TValue>, IReadOnlyList<TValue>, IList
{
public int Count { get; }
public void CopyTo(TValue[] array, int arrayIndex);
public OrderedDictionary<TKey, TValue>.ValueCollection.Enumerator GetEnumerator();
public struct Enumerator : IEnumerator<TValue>
{
public TValue Current { get; }
public bool MoveNext();
public void Dispose();
}
}
}
Perhaps one of the reasons there was no generic OrderedDictionary
added initially was due to issues with having both a key and index indexer when the key is an int
. A call to the indexer would be ambiguous. Roslyn prefers the non-generic parameter so in this case the index indexer will be called.
API Details
Insert
allowsindex
to be equal toCount
to insert the element at the end.SetAt(int index, TValue value)
requiresindex
to be less thanCount
butSetAt(int index, TKey key, TValue value)
allowsindex
to be equal toCount
similar toInsert
.- Performance will be the same as
Dictionary
for all operations exceptRemove
which will necessarily beO(n)
.Insert
andRemoveAt
which aren't members ofDictionary
will also beO(n)
.
Open Questions
- Should the namespace be
System.Collections.Generic
when it could easily beSystem.Collections.Specialized
where the non-generic version is located? I just felt this collection is far more useful to be relegated to that namespace. - Should the non-generic interfaces
ICollection
,IList
, andIOrderedDictionary
be implemented?
Updates
- Added constructor overloads for
IEnumerable<KeyValuePair<TKey, TValue>>
. - Added
ContainsValue
method due to being needed for theValueCollection.Contains
method. - Proposal no longer advocates for throwing an exception when using an indexer while the key is an int.