Skip to content

[API Proposal]: Add a generic OrderedDictionary class #24826

Closed
@TylerBrinkley

Description

@TylerBrinkley

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 allows index to be equal to Count to insert the element at the end.
  • SetAt(int index, TValue value) requires index to be less than Count but SetAt(int index, TKey key, TValue value) allows index to be equal to Count similar to Insert.
  • Performance will be the same as Dictionary for all operations except Remove which will necessarily be O(n). Insert and RemoveAt which aren't members of Dictionary will also be O(n).

Open Questions

  • Should the namespace be System.Collections.Generic when it could easily be System.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, and IOrderedDictionary be implemented?

Updates

  • Added constructor overloads for IEnumerable<KeyValuePair<TKey, TValue>>.
  • Added ContainsValue method due to being needed for the ValueCollection.Contains method.
  • Proposal no longer advocates for throwing an exception when using an indexer while the key is an int.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Collectionsin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions