166

There doesn't appear to be a generic implementation of OrderedDictionary (which is in the System.Collections.Specialized namespace) in .NET 3.5. Is there one that I'm missing?

I've found implementations out there to provide the functionality, but wondered if/why there isn't a generic implementation out-of-the-box and if anyone knows whether it's something in .NET 4.0?

Danny Beckett
  • 20,529
  • 24
  • 107
  • 134
AdaTheDev
  • 142,592
  • 28
  • 206
  • 200
  • 1
    Here is an implementation of a `OrderedDictionary`: http://www.codeproject.com/Articles/18615/OrderedDictionary-T-A-generic-implementation-of-IO – Tim Schmelter Feb 01 '12 at 21:54
  • 2
    possible duplicate of [Generic Key/Value pair collection in that preserves insertion order?](http://stackoverflow.com/questions/1396718/generic-key-value-pair-collection-in-that-preserves-insertion-order) – nawfal Oct 31 '13 at 07:07
  • My implementation of OrderedDictionary has O(1) insert/delete because it uses a LinkedList instead of ArrayList to maintain insertion order: http://clintonbrennan.com/2013/12/generic-ordereddictionary-implemented-in-c-dotnet/ – Clinton Dec 20 '13 at 21:23
  • 3
    If you just need to be able to iterate over the entries in the order they were added then List> may be good enough. (Granted, not a general solution, but good enough for some purposes.) – yoyo Jul 15 '14 at 06:04
  • 1
    It's an unfortunate omission. There are other good data types `Systems.Collections.Generic`. Let's request `OrderedDictionary` for .NET 5. As others have pointed out, the case the key is an int is degenerate, and will need special care. – Colonel Panic Sep 30 '14 at 11:02
  • There's another fast implementation, based on Dictionary source code. https://github.com/OndrejPetrzilka/Rock.Collections – Ondrej Petrzilka Oct 21 '16 at 18:11

13 Answers13

105

Implementing a generic OrderedDictionary isn't terribly difficult, but it's unnecessarily time consuming and frankly this class is a huge oversight on Microsoft's part. There are multiple ways of implementing this, but I chose to use a KeyedCollection for my internal storage. I also chose to implement various methods for sorting the way that List<T> does since this is essentially a hybrid IList and IDictionary. I've included my implementation here for posterity.

Here's the interface. Notice that it includes System.Collections.Specialized.IOrderedDictionary, which is the non-generic version of this interface that was provided by Microsoft.

// http://unlicense.org
using System;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace mattmc3.Common.Collections.Generic {

    public interface IOrderedDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IOrderedDictionary {
        new TValue this[int index] { get; set; }
        new TValue this[TKey key] { get; set; }
        new int Count { get; }
        new ICollection<TKey> Keys { get; }
        new ICollection<TValue> Values { get; }
        new void Add(TKey key, TValue value);
        new void Clear();
        void Insert(int index, TKey key, TValue value);
        int IndexOf(TKey key);
        bool ContainsValue(TValue value);
        bool ContainsValue(TValue value, IEqualityComparer<TValue> comparer);
        new bool ContainsKey(TKey key);
        new IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
        new bool Remove(TKey key);
        new void RemoveAt(int index);
        new bool TryGetValue(TKey key, out TValue value);
        TValue GetValue(TKey key);
        void SetValue(TKey key, TValue value);
        KeyValuePair<TKey, TValue> GetItem(int index);
        void SetItem(int index, TValue value);
    }

}

Here's the implementation along with helper classes:

// http://unlicense.org
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Linq;

namespace mattmc3.Common.Collections.Generic {

    /// <summary>
    /// A dictionary object that allows rapid hash lookups using keys, but also
    /// maintains the key insertion order so that values can be retrieved by
    /// key index.
    /// </summary>
    public class OrderedDictionary<TKey, TValue> : IOrderedDictionary<TKey, TValue> {

        #region Fields/Properties

        private KeyedCollection2<TKey, KeyValuePair<TKey, TValue>> _keyedCollection;

        /// <summary>
        /// Gets or sets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key associated with the value to get or set.</param>
        public TValue this[TKey key] {
            get {
                return GetValue(key);
            }
            set {
                SetValue(key, value);
            }
        }

        /// <summary>
        /// Gets or sets the value at the specified index.
        /// </summary>
        /// <param name="index">The index of the value to get or set.</param>
        public TValue this[int index] {
            get {
                return GetItem(index).Value;
            }
            set {
                SetItem(index, value);
            }
        }

        public int Count {
            get { return _keyedCollection.Count; }
        }

        public ICollection<TKey> Keys {
            get {
                return _keyedCollection.Select(x => x.Key).ToList();
            }
        }

        public ICollection<TValue> Values {
            get {
                return _keyedCollection.Select(x => x.Value).ToList();
            }
        }

        public IEqualityComparer<TKey> Comparer {
            get;
            private set;
        }

        #endregion

        #region Constructors

        public OrderedDictionary() {
            Initialize();
        }

        public OrderedDictionary(IEqualityComparer<TKey> comparer) {
            Initialize(comparer);
        }

        public OrderedDictionary(IOrderedDictionary<TKey, TValue> dictionary) {
            Initialize();
            foreach (KeyValuePair<TKey, TValue> pair in dictionary) {
                _keyedCollection.Add(pair);
            }
        }

        public OrderedDictionary(IOrderedDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) {
            Initialize(comparer);
            foreach (KeyValuePair<TKey, TValue> pair in dictionary) {
                _keyedCollection.Add(pair);
            }
        }

        #endregion

        #region Methods

        private void Initialize(IEqualityComparer<TKey> comparer = null) {
            this.Comparer = comparer;
            if (comparer != null) {
                _keyedCollection = new KeyedCollection2<TKey, KeyValuePair<TKey, TValue>>(x => x.Key, comparer);
            }
            else {
                _keyedCollection = new KeyedCollection2<TKey, KeyValuePair<TKey, TValue>>(x => x.Key);
            }
        }

        public void Add(TKey key, TValue value) {
            _keyedCollection.Add(new KeyValuePair<TKey, TValue>(key, value));
        }

        public void Clear() {
            _keyedCollection.Clear();
        }

        public void Insert(int index, TKey key, TValue value) {
            _keyedCollection.Insert(index, new KeyValuePair<TKey, TValue>(key, value));
        }

        public int IndexOf(TKey key) {
            if (_keyedCollection.Contains(key)) {
                return _keyedCollection.IndexOf(_keyedCollection[key]);
            }
            else {
                return -1;
            }
        }

        public bool ContainsValue(TValue value) {
            return this.Values.Contains(value);
        }

        public bool ContainsValue(TValue value, IEqualityComparer<TValue> comparer) {
            return this.Values.Contains(value, comparer);
        }

        public bool ContainsKey(TKey key) {
            return _keyedCollection.Contains(key);
        }

        public KeyValuePair<TKey, TValue> GetItem(int index) {
            if (index < 0 || index >= _keyedCollection.Count) {
                throw new ArgumentException(String.Format("The index was outside the bounds of the dictionary: {0}", index));
            }
            return _keyedCollection[index];
        }

        /// <summary>
        /// Sets the value at the index specified.
        /// </summary>
        /// <param name="index">The index of the value desired</param>
        /// <param name="value">The value to set</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown when the index specified does not refer to a KeyValuePair in this object
        /// </exception>
        public void SetItem(int index, TValue value) {
            if (index < 0 || index >= _keyedCollection.Count) {
                throw new ArgumentException("The index is outside the bounds of the dictionary: {0}".FormatWith(index));
            }
            var kvp = new KeyValuePair<TKey, TValue>(_keyedCollection[index].Key, value);
            _keyedCollection[index] = kvp;
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
            return _keyedCollection.GetEnumerator();
        }

        public bool Remove(TKey key) {
            return _keyedCollection.Remove(key);
        }

        public void RemoveAt(int index) {
            if (index < 0 || index >= _keyedCollection.Count) {
                throw new ArgumentException(String.Format("The index was outside the bounds of the dictionary: {0}", index));
            }
            _keyedCollection.RemoveAt(index);
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key associated with the value to get.</param>
        public TValue GetValue(TKey key) {
            if (_keyedCollection.Contains(key) == false) {
                throw new ArgumentException("The given key is not present in the dictionary: {0}".FormatWith(key));
            }
            var kvp = _keyedCollection[key];
            return kvp.Value;
        }

        /// <summary>
        /// Sets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key associated with the value to set.</param>
        /// <param name="value">The the value to set.</param>
        public void SetValue(TKey key, TValue value) {
            var kvp = new KeyValuePair<TKey, TValue>(key, value);
            var idx = IndexOf(key);
            if (idx > -1) {
                _keyedCollection[idx] = kvp;
            }
            else {
                _keyedCollection.Add(kvp);
            }
        }

        public bool TryGetValue(TKey key, out TValue value) {
            if (_keyedCollection.Contains(key)) {
                value = _keyedCollection[key].Value;
                return true;
            }
            else {
                value = default(TValue);
                return false;
            }
        }

        #endregion

        #region sorting
        public void SortKeys() {
            _keyedCollection.SortByKeys();
        }

        public void SortKeys(IComparer<TKey> comparer) {
            _keyedCollection.SortByKeys(comparer);
        }

        public void SortKeys(Comparison<TKey> comparison) {
            _keyedCollection.SortByKeys(comparison);
        }

        public void SortValues() {
            var comparer = Comparer<TValue>.Default;
            SortValues(comparer);
        }

        public void SortValues(IComparer<TValue> comparer) {
            _keyedCollection.Sort((x, y) => comparer.Compare(x.Value, y.Value));
        }

        public void SortValues(Comparison<TValue> comparison) {
            _keyedCollection.Sort((x, y) => comparison(x.Value, y.Value));
        }
        #endregion

        #region IDictionary<TKey, TValue>

        void IDictionary<TKey, TValue>.Add(TKey key, TValue value) {
            Add(key, value);
        }

        bool IDictionary<TKey, TValue>.ContainsKey(TKey key) {
            return ContainsKey(key);
        }

        ICollection<TKey> IDictionary<TKey, TValue>.Keys {
            get { return Keys; }
        }

        bool IDictionary<TKey, TValue>.Remove(TKey key) {
            return Remove(key);
        }

        bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) {
            return TryGetValue(key, out value);
        }

        ICollection<TValue> IDictionary<TKey, TValue>.Values {
            get { return Values; }
        }

        TValue IDictionary<TKey, TValue>.this[TKey key] {
            get {
                return this[key];
            }
            set {
                this[key] = value;
            }
        }

        #endregion

        #region ICollection<KeyValuePair<TKey, TValue>>

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item) {
            _keyedCollection.Add(item);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear() {
            _keyedCollection.Clear();
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item) {
            return _keyedCollection.Contains(item);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
            _keyedCollection.CopyTo(array, arrayIndex);
        }

        int ICollection<KeyValuePair<TKey, TValue>>.Count {
            get { return _keyedCollection.Count; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly {
            get { return false; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item) {
            return _keyedCollection.Remove(item);
        }

        #endregion

        #region IEnumerable<KeyValuePair<TKey, TValue>>

        IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() {
            return GetEnumerator();
        }

        #endregion

        #region IEnumerable

        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        #endregion

        #region IOrderedDictionary

        IDictionaryEnumerator IOrderedDictionary.GetEnumerator() {
            return new DictionaryEnumerator<TKey, TValue>(this);
        }

        void IOrderedDictionary.Insert(int index, object key, object value) {
            Insert(index, (TKey)key, (TValue)value);
        }

        void IOrderedDictionary.RemoveAt(int index) {
            RemoveAt(index);
        }

        object IOrderedDictionary.this[int index] {
            get {
                return this[index];
            }
            set {
                this[index] = (TValue)value;
            }
        }

        #endregion

        #region IDictionary

        void IDictionary.Add(object key, object value) {
            Add((TKey)key, (TValue)value);
        }

        void IDictionary.Clear() {
            Clear();
        }

        bool IDictionary.Contains(object key) {
            return _keyedCollection.Contains((TKey)key);
        }

        IDictionaryEnumerator IDictionary.GetEnumerator() {
            return new DictionaryEnumerator<TKey, TValue>(this);
        }

        bool IDictionary.IsFixedSize {
            get { return false; }
        }

        bool IDictionary.IsReadOnly {
            get { return false; }
        }

        ICollection IDictionary.Keys {
            get { return (ICollection)this.Keys; }
        }

        void IDictionary.Remove(object key) {
            Remove((TKey)key);
        }

        ICollection IDictionary.Values {
            get { return (ICollection)this.Values; }
        }

        object IDictionary.this[object key] {
            get {
                return this[(TKey)key];
            }
            set {
                this[(TKey)key] = (TValue)value;
            }
        }

        #endregion

        #region ICollection

        void ICollection.CopyTo(Array array, int index) {
            ((ICollection)_keyedCollection).CopyTo(array, index);
        }

        int ICollection.Count {
            get { return ((ICollection)_keyedCollection).Count; }
        }

        bool ICollection.IsSynchronized {
            get { return ((ICollection)_keyedCollection).IsSynchronized; }
        }

        object ICollection.SyncRoot {
            get { return ((ICollection)_keyedCollection).SyncRoot; }
        }

        #endregion
    }

    public class KeyedCollection2<TKey, TItem> : KeyedCollection<TKey, TItem> {
        private const string DelegateNullExceptionMessage = "Delegate passed cannot be null";
        private Func<TItem, TKey> _getKeyForItemDelegate;

        public KeyedCollection2(Func<TItem, TKey> getKeyForItemDelegate)
            : base() {
            if (getKeyForItemDelegate == null) throw new ArgumentNullException(DelegateNullExceptionMessage);
            _getKeyForItemDelegate = getKeyForItemDelegate;
        }

        public KeyedCollection2(Func<TItem, TKey> getKeyForItemDelegate, IEqualityComparer<TKey> comparer)
            : base(comparer) {
            if (getKeyForItemDelegate == null) throw new ArgumentNullException(DelegateNullExceptionMessage);
            _getKeyForItemDelegate = getKeyForItemDelegate;
        }

        protected override TKey GetKeyForItem(TItem item) {
            return _getKeyForItemDelegate(item);
        }

        public void SortByKeys() {
            var comparer = Comparer<TKey>.Default;
            SortByKeys(comparer);
        }

        public void SortByKeys(IComparer<TKey> keyComparer) {
            var comparer = new Comparer2<TItem>((x, y) => keyComparer.Compare(GetKeyForItem(x), GetKeyForItem(y)));
            Sort(comparer);
        }

        public void SortByKeys(Comparison<TKey> keyComparison) {
            var comparer = new Comparer2<TItem>((x, y) => keyComparison(GetKeyForItem(x), GetKeyForItem(y)));
            Sort(comparer);
        }

        public void Sort() {
            var comparer = Comparer<TItem>.Default;
            Sort(comparer);
        }

        public void Sort(Comparison<TItem> comparison) {
            var newComparer = new Comparer2<TItem>((x, y) => comparison(x, y));
            Sort(newComparer);
        }

        public void Sort(IComparer<TItem> comparer) {
            List<TItem> list = base.Items as List<TItem>;
            if (list != null) {
                list.Sort(comparer);
            }
        }
    }

    public class Comparer2<T> : Comparer<T> {
        //private readonly Func<T, T, int> _compareFunction;
        private readonly Comparison<T> _compareFunction;

        #region Constructors

        public Comparer2(Comparison<T> comparison) {
            if (comparison == null) throw new ArgumentNullException("comparison");
            _compareFunction = comparison;
        }

        #endregion

        public override int Compare(T arg1, T arg2) {
            return _compareFunction(arg1, arg2);
        }
    }

    public class DictionaryEnumerator<TKey, TValue> : IDictionaryEnumerator, IDisposable {
        readonly IEnumerator<KeyValuePair<TKey, TValue>> impl;
        public void Dispose() { impl.Dispose(); }
        public DictionaryEnumerator(IDictionary<TKey, TValue> value) {
            this.impl = value.GetEnumerator();
        }
        public void Reset() { impl.Reset(); }
        public bool MoveNext() { return impl.MoveNext(); }
        public DictionaryEntry Entry {
            get {
                var pair = impl.Current;
                return new DictionaryEntry(pair.Key, pair.Value);
            }
        }
        public object Key { get { return impl.Current.Key; } }
        public object Value { get { return impl.Current.Value; } }
        public object Current { get { return Entry; } }
    }
}

And no implementation would be complete without a few tests (but tragically, SO won't let me post that much code in one post), so I'll have to leave you to write your tests. But, I left a few of them in so that you could get an idea of how it works:

// http://unlicense.org
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using mattmc3.Common.Collections.Generic;

namespace mattmc3.Tests.Common.Collections.Generic {
    [TestClass]
    public class OrderedDictionaryTests {

        private OrderedDictionary<string, string> GetAlphabetDictionary(IEqualityComparer<string> comparer = null) {
            OrderedDictionary<string, string> alphabet = (comparer == null ? new OrderedDictionary<string, string>() : new OrderedDictionary<string, string>(comparer));
            for (var a = Convert.ToInt32('a'); a <= Convert.ToInt32('z'); a++) {
                var c = Convert.ToChar(a);
                alphabet.Add(c.ToString(), c.ToString().ToUpper());
            }
            Assert.AreEqual(26, alphabet.Count);
            return alphabet;
        }

        private List<KeyValuePair<string, string>> GetAlphabetList() {
            var alphabet = new List<KeyValuePair<string, string>>();
            for (var a = Convert.ToInt32('a'); a <= Convert.ToInt32('z'); a++) {
                var c = Convert.ToChar(a);
                alphabet.Add(new KeyValuePair<string, string>(c.ToString(), c.ToString().ToUpper()));
            }
            Assert.AreEqual(26, alphabet.Count);
            return alphabet;
        }

        [TestMethod]
        public void TestAdd() {
            var od = new OrderedDictionary<string, string>();
            Assert.AreEqual(0, od.Count);
            Assert.AreEqual(-1, od.IndexOf("foo"));

            od.Add("foo", "bar");
            Assert.AreEqual(1, od.Count);
            Assert.AreEqual(0, od.IndexOf("foo"));
            Assert.AreEqual(od[0], "bar");
            Assert.AreEqual(od["foo"], "bar");
            Assert.AreEqual(od.GetItem(0).Key, "foo");
            Assert.AreEqual(od.GetItem(0).Value, "bar");
        }

        [TestMethod]
        public void TestRemove() {
            var od = new OrderedDictionary<string, string>();

            od.Add("foo", "bar");
            Assert.AreEqual(1, od.Count);

            od.Remove("foo");
            Assert.AreEqual(0, od.Count);
        }

        [TestMethod]
        public void TestRemoveAt() {
            var od = new OrderedDictionary<string, string>();

            od.Add("foo", "bar");
            Assert.AreEqual(1, od.Count);

            od.RemoveAt(0);
            Assert.AreEqual(0, od.Count);
        }

        [TestMethod]
        public void TestClear() {
            var od = GetAlphabetDictionary();
            Assert.AreEqual(26, od.Count);
            od.Clear();
            Assert.AreEqual(0, od.Count);
        }

        [TestMethod]
        public void TestOrderIsPreserved() {
            var alphabetDict = GetAlphabetDictionary();
            var alphabetList = GetAlphabetList();
            Assert.AreEqual(26, alphabetDict.Count);
            Assert.AreEqual(26, alphabetList.Count);

            var keys = alphabetDict.Keys.ToList();
            var values = alphabetDict.Values.ToList();

            for (var i = 0; i < 26; i++) {
                var dictItem = alphabetDict.GetItem(i);
                var listItem = alphabetList[i];
                var key = keys[i];
                var value = values[i];

                Assert.AreEqual(dictItem, listItem);
                Assert.AreEqual(key, listItem.Key);
                Assert.AreEqual(value, listItem.Value);
            }
        }

        [TestMethod]
        public void TestTryGetValue() {
            var alphabetDict = GetAlphabetDictionary();
            string result = null;
            Assert.IsFalse(alphabetDict.TryGetValue("abc", out result));
            Assert.IsNull(result);
            Assert.IsTrue(alphabetDict.TryGetValue("z", out result));
            Assert.AreEqual("Z", result);
        }

        [TestMethod]
        public void TestEnumerator() {
            var alphabetDict = GetAlphabetDictionary();

            var keys = alphabetDict.Keys.ToList();
            Assert.AreEqual(26, keys.Count);

            var i = 0;
            foreach (var kvp in alphabetDict) {
                var value = alphabetDict[kvp.Key];
                Assert.AreEqual(kvp.Value, value);
                i++;
            }
        }

        [TestMethod]
        public void TestInvalidIndex() {
            var alphabetDict = GetAlphabetDictionary();
            try {
                var notGonnaWork = alphabetDict[100];
                Assert.IsTrue(false, "Exception should have thrown");
            }
            catch (Exception ex) {
                Assert.IsTrue(ex.Message.Contains("index is outside the bounds"));
            }
        }

        [TestMethod]
        public void TestMissingKey() {
            var alphabetDict = GetAlphabetDictionary();
            try {
                var notGonnaWork = alphabetDict["abc"];
                Assert.IsTrue(false, "Exception should have thrown");
            }
            catch (Exception ex) {
                Assert.IsTrue(ex.Message.Contains("key is not present"));
            }
        }

        [TestMethod]
        public void TestUpdateExistingValue() {
            var alphabetDict = GetAlphabetDictionary();
            Assert.IsTrue(alphabetDict.ContainsKey("c"));
            Assert.AreEqual(2, alphabetDict.IndexOf("c"));
            Assert.AreEqual(alphabetDict[2], "C");
            alphabetDict[2] = "CCC";
            Assert.IsTrue(alphabetDict.ContainsKey("c"));
            Assert.AreEqual(2, alphabetDict.IndexOf("c"));
            Assert.AreEqual(alphabetDict[2], "CCC");
        }

        [TestMethod]
        public void TestInsertValue() {
            var alphabetDict = GetAlphabetDictionary();
            Assert.IsTrue(alphabetDict.ContainsKey("c"));
            Assert.AreEqual(2, alphabetDict.IndexOf("c"));
            Assert.AreEqual(alphabetDict[2], "C");
            Assert.AreEqual(26, alphabetDict.Count);
            Assert.IsFalse(alphabetDict.ContainsValue("ABC"));

            alphabetDict.Insert(2, "abc", "ABC");
            Assert.IsTrue(alphabetDict.ContainsKey("c"));
            Assert.AreEqual(2, alphabetDict.IndexOf("abc"));
            Assert.AreEqual(alphabetDict[2], "ABC");
            Assert.AreEqual(27, alphabetDict.Count);
            Assert.IsTrue(alphabetDict.ContainsValue("ABC"));
        }

        [TestMethod]
        public void TestValueComparer() {
            var alphabetDict = GetAlphabetDictionary();
            Assert.IsFalse(alphabetDict.ContainsValue("a"));
            Assert.IsTrue(alphabetDict.ContainsValue("a", StringComparer.OrdinalIgnoreCase));
        }

        [TestMethod]
        public void TestSortByKeys() {
            var alphabetDict = GetAlphabetDictionary();
            var reverseAlphabetDict = GetAlphabetDictionary();
            Comparison<string> stringReverse = ((x, y) => (String.Equals(x, y) ? 0 : String.Compare(x, y) >= 1 ? -1 : 1));
            reverseAlphabetDict.SortKeys(stringReverse);
            for (int j = 0, k = 25; j < alphabetDict.Count; j++, k--) {
                var ascValue = alphabetDict.GetItem(j);
                var dscValue = reverseAlphabetDict.GetItem(k);
                Assert.AreEqual(ascValue.Key, dscValue.Key);
                Assert.AreEqual(ascValue.Value, dscValue.Value);
            }
        }

-- UPDATE --

Source for this and other really useful missing core .NET libraries here: https://github.com/mattmc3/dotmore/blob/master/dotmore/Collections/Generic/OrderedDictionary.cs

mattmc3
  • 17,595
  • 7
  • 83
  • 103
  • Thanks. Are you releasing this to the public domain? – Colonel Panic Mar 01 '13 at 13:43
  • 7
    By public domain, are you asking if you can use it, modify it, and treat it like it's yours worry free - yes. Feel free. If you mean license it and host it somewhere - no... it lives here on SO only for now. – mattmc3 Mar 05 '13 at 03:43
  • 3
    @mattmc3 Thank you for your code sir, but your comment on public domain issues concerns me, when you said in the comment: "If you mean license it and host it somewhere - no... it lives here on SO only for now." With all respect (truly meant) to the author, do you really have a right to make that restriction, once you've posted the code on SO??? E.g. do any of us really have the right to restrict our code on SO from being posted, for instance, as a git gist? Anyone? – Nicholas Petersen Sep 08 '13 at 16:30
  • 7
    @NicholasPetersen - I think you misunderstood. In direct response to Colonel Panic, I informed him that *I personally* did not license it or host it anywhere else. (Actually, since you mentioned it, I did make a gist: https://gist.github.com/mattmc3/6486878). But this is license free code. Take it and do what you want with it. I wrote it 100% for my own personal use. It's unencumbered. Enjoy. In fact, if someone from Microsoft ever reads this, I fully expect them to do their duty and finally put it in the next release of .NET. No attributions necessary. – mattmc3 Sep 08 '13 at 17:50
  • @mattmc3 Thanks for the clarification! And you had already made a gist of it, sweet. Yep, we expect you to do your duty Microsoft, and adopt some of these things (*not* holding my breath). – Nicholas Petersen Sep 09 '13 at 04:19
  • 3
    What if `TKey` is `int`? How `this[]` will work in such a case? – V.B. Nov 20 '13 at 06:36
  • @V.B. - That's a great question - it will call `this[int index]`, leaving you unable to call `this[TKey key]`. I've updated the interface to contain `GetValue()` and `SetValue()` methods to overcome this issue. If you choose an `int` for TKey, you'll need to do key lookups using those methods. – mattmc3 Nov 20 '13 at 22:31
  • 1
    As @supercat says below, this is probably one of the reasons OD is not generic. In OD a call to this[obj] is clearly differentiated from this[int] – V.B. Nov 21 '13 at 04:34
  • `But this is license free code. Take it and do what you want with it. I wrote it 100% for my own personal use. It's unencumbered.`—if you could just more clearly state, within your post, that “I, mattmc3, hereby release this into the public domain”, that would probably make people a lot more comfortable using it. You keep saying that you haven’t released it or that you give it no license in your comments, so confusing! Also, pointing to a nuget package would be much nicer than saying “please copy/paste because forks” ;-) – binki Jul 03 '14 at 14:35
  • 1
    The beautiful thing about "zero license code" is, if you want it in nuget - plop it in your own nuget package and call it yours. No attribution necessary. – mattmc3 May 25 '15 at 18:29
  • 1
    Why did you `new` all the methods in the interface? It's completely unnecessarily, it just forces you to write explicit interface implementations of the old methods to map them to the new ones. – Thomas Levesque Jul 27 '15 at 22:55
  • @ThomasLevesque I fully agree with you on that one. For instance, the IDictionary interface is not hiding the non generic IDictionary interface methods, same for IList and IList, ICollection and ICollection. It's not ~necessarily~ wise and clean to make up a generic collection interface based on a non-generic one. API-wise, it makes things even worse and push developers to use obsolete interfaces for the so-called sake of backward compatibility (I'm not saying that was the initial intention though). I don't see hiding as a necessary evil here. – Natalie Perret Nov 25 '15 at 11:02
  • 1
    @ThomasLevesque and @EhouarnPerret - The decision to `new` all the methods was solely to implement the built-in non-generic `IOrderedDictionary` interface, because the return value needed to be changed from `object` to `TValue`. To my knowledge, you cannot do that without masking. If you prefer not to implement the non-generic IOrderedDictionary interface, that's your choice, but no - it's not "completely unnecessary". The ability to cast to the built-in IOrderedDictionary is an important feature of this implementation. – mattmc3 Nov 25 '15 at 16:17
  • @mattmc3, I see; I didn't realized you were also implementing the non-generic interface – Thomas Levesque Nov 25 '15 at 17:34
  • 1
    @doug65536 - To put this issue to bed: http://unlicense.org . Or, http://www.wtfpl.net if you please. This post finally convinced me I had to be clearer, despite having already said it multiple times: https://blog.codinghorror.com/pick-a-license-any-license/ – mattmc3 Oct 06 '16 at 14:30
  • 2
    @mattmc3 Sharing this, and especially your whole dotmore library, is really an inspiration towards the code I am able to share. I know many people share many libraries between small and huge size, but still, it always deserves way more than the gratitude. – Regular Jo Dec 20 '18 at 06:42
  • This is very useful! I do have one question; what would be the best way to retrieve the key at position *n*? I could create a new list with the keys (`new List(_orderedDictionary.Keys)[0]`) and grab it based on the index, however I'm not sure if this retains the order, and it'll also likely add unnecessary overhead. – devklick Feb 09 '19 at 02:24
  • 2
    @klicker - Just use regular array style indexing. Insertion order is maintained just like a list. The type conversion handles determining whether you meant to index with an int, or to reference via the key’s type. If the key’s type is an int, then you need to use the GetValue() method. – mattmc3 Feb 10 '19 at 03:03
  • Interestingly, it seems like a lot of your `dotmore` classes are no longer needed. For instance, `Comparer2`. – Sinjai Feb 28 '19 at 22:14
  • 2
    To anyone interested in this a decade later: I did put an updated version [here](https://gist.github.com/fdcastel/55672ad0dc07812522fc3b4a8c8d1668) – F.D.Castel May 27 '23 at 20:10
67

You're right. There's no generic equivalent of OrderedDictionary in the framework itself.

(That's still the case for .NET 4 too, as far as I'm aware.)

General Grievance
  • 4,555
  • 31
  • 31
  • 45
LukeH
  • 263,068
  • 57
  • 365
  • 409
41

For the record, there is a generic KeyedCollection that allows objects to be indexed by an int and a key. The key must be embedded in the value.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Guillaume
  • 12,824
  • 3
  • 40
  • 48
  • 2
    This does not maintain the order of initialization like OrderedDictionary! Check out my answer. – JoelFan Dec 21 '11 at 17:30
  • 18
    It does maintain the order of add/insertion. – Guillaume Dec 22 '11 at 08:55
  • yes it does.. where do you guys got this notion that the keyedcollection sorts items... i am stumbled upon this second time – Boppity Bop Jan 20 '13 at 20:12
  • 1
    It definitely does maintain the the order of initialization. Helpful links include http://stackoverflow.com/a/11802824/9344 and http://geekswithblogs.net/NewThingsILearned/archive/2010/01/07/using-keyedcollectionlttkey-titemgt.aspx. – Ted Apr 01 '15 at 00:32
  • +1, This seems like the best solution in the framework. Having to implement the abstract class for each pair of types you want to use is kind of a drag, though. You could do it with one generic implementation that requires an interface, but then you'd have to implement the interface on each type you wanted to be able to store. – DCShannon Jul 30 '15 at 02:07
  • 2
    @DCShannon, you can write an implementation requiring a Func avoiding the need to implement an interface/override the method. – Guillaume Jul 31 '15 at 05:52
  • @Guillaume Yeah, that'd work. Just pass that into the constructor. Good idea. – DCShannon Jul 31 '15 at 20:16
  • Thanks! I came here looking for a generic OrderedDictionary (and will probably use it another day), but KeyedCollection is actually a better fit for what I need today. KeyedCollection had sorta whizzed by me as the base class for KeyedByTypeCollection, which I had used before, but I never stopped to look at KeyedCollection itself (before today). – unbob Aug 24 '20 at 12:15
22

Here's a bizarre find: the System.Web.Util namespace in System.Web.Extensions.dll contains a generic OrderedDictionary<TKey,TValue>

// Type: System.Web.Util.OrderedDictionary`2
// Assembly: System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Web.Extensions.dll

namespace System.Web.Util
{
    internal class OrderedDictionary<TKey, TValue> : IDictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable

Not sure why MS placed it there instead of the System.Collections.Generic package, but I assume you can simply copy paste the code and use it (it's internal, so can't use it directly). Looks like the implementation uses a standard dictionary and separate Key/Value lists. Pretty straightforward...

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Ismail Degani
  • 857
  • 9
  • 15
  • 3
    `System.Runtime.Collections` also contains an internal `OrderedDictionary` which just wraps around the non-generic version – V.B. Nov 21 '13 at 04:38
  • 1
    `System.Web.Util.OrderedDictionary` is internally implemented around generic `Dictionary`. Oddly it does not implement `IList` but `ICollection>` – Mikhail Poda Jan 29 '15 at 11:10
  • The license issue is incorrect. That does not apply to method/function definitions. Only to method function implementations. Otherwise we wouldn't have projects such as Mono and Wine (one person looks at the definitions, also seeing the implementation. The other person implements the definition with the purpose without seeing the implementation - and as long as you don't violate any patents (must be an algorithm), this is ok). – TamusJRoyce Dec 09 '15 at 17:34
  • @V.B. how does one reference System.Runtime.Collections, I can't seem to find it anywhere (using .NET 4.0 Client Profile) – rboy Jun 24 '17 at 14:23
  • 1
    @rboy As I said - it was internal and wrapped the non-generic implementation. But it was 3+ years ago... For sizes below couple of hundreds linear search on `List>` will be competitive due to memory access pattern, for larger sizes just use the same list + `Dictionary` as a lookup. AFAIK there is no such a datastructure that does better in speed/memory terms in BigO. – V.B. Jun 24 '17 at 18:27
  • 1
    @rboy here is the link to the [generic one](https://referencesource.microsoft.com/#System.ServiceModel.Internals/System/Runtime/Collections/OrderedDictionary.cs,6c358d08d62d3514), it references [non-generic one](https://referencesource.microsoft.com/#System/compmod/system/collections/specialized/ordereddictionary.cs,26ddc1f91eaaa246) that uses HashTable. I really bet that for small sizes using linear search on generic List/Arrays will be faster. – V.B. Jun 24 '17 at 18:30
  • @V.B. that's gr8 tx. Oddly `List>` is NOT the same as `SortedDictionary`. See my question and answer here, the two aren't interchangeable and no one is able to answer why, it just doesn't work at runtime. https://stackoverflow.com/a/44731777 – rboy Jun 27 '17 at 02:14
  • @rboy Sorted and Ordered are completely different things, you asked for Ordered one. And you didn't mention JSON.NET here... Use a custom converter and parse manually - that is easier than it may sound. – V.B. Jun 27 '17 at 03:10
  • Sorry typo. I meant `OrderedDictionary` but the underlying issue still remains as to why List and Keypair won't work. I know it's off topic. – rboy Jun 27 '17 at 11:57
  • How does it compare to *[System.Collections.Specialized.OrderedDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.specialized.ordereddictionary)* (same name, `OrderedDictionary`)? – Peter Mortensen Jan 20 '19 at 12:27
  • 2
    @PeterMortensen `System.Collections.Specialized.OrderedDictionary` is not a generic type. Look ma, no angle brackets at the doc page you linked :P – user7610 Mar 04 '19 at 08:09
17

A major conceptual problem with a generic version of OrderedDictionary is that users of a OrderedDictionary<TKey,TValue> would expect expect to be able to index it either numerically using an int, or by lookup using a TKey. When the only type of key was Object, as was the case with non-generic OrderedDictionary, the type of argument passed to the indexer would be sufficient to distinguish whether what type of indexing operation should be performed. As it is, though, it's unclear how the indexer of an OrderedDictionary<int, TValue> should behave.

If classes like Drawing.Point had recommended and followed a rule that piecewise-mutable structures should expose their mutable elements as fields rather than properties, and refrain from using property setters that modify this, then an OrderedDictionary<TKey,TValue> could efficiently expose a ByIndex property that returned an Indexer struct which held a reference to the dictionary, and had an indexed property whose getter and setter would call GetByIndex and SetByIndex upon it. Thus, one could say something like MyDict.ByIndex[5] += 3; to add 3 to the sixth element of the dictionary.

Unfortunately, for the compiler to accept such a thing, it would be necessary to make the ByIndex property return a new class instance rather than a struct every time it's invoked, eliminating the advantages one would get by avoiding boxing.

In VB.NET, one could get around that issue by using a named indexed property (so MyDict.ByIndex[int] would be a member of MyDict, rather than requiring MyDict.ByIndex to be a member of MyDict which includes an indexer), but C# doesn't allow such things.

It might still have been worthwhile to offer an OrderedDictionary<TKey,TValue> where TKey:class, but much of the reason for providing generics in the first place was to allow their use with value types.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
supercat
  • 77,689
  • 9
  • 166
  • 211
  • Good point that `int`-typed keys present a challenge, but it could be avoided by following the example of the related `SortedList` type: support keys only with `[...]`, and require use of `.Values[...]` for access by index. (`SortedDictionary`, by contrast, which isn't optimized for indexed access, requires use of `.ElementAt(...).Value`) – mklement0 Aug 20 '19 at 16:50
  • This (int as key vs int as index) comes up (for me) a lot in PowerShell. More recent versions of PowerShell give easy access to untyped OrderedDictionary via [ordered]@{}. My tacky workaround to get int as key and index is to cast the int to long when I want it as a key. But, now that you've got me thinking about this. . .PowerShell exposes the indexer as Item() and $odict.Item([object]$key) will force PowerShell to use an int as key. More verbose, but less hacky. Thanks! – unbob Aug 24 '20 at 12:08
  • If you provide an integer to an indexer then IMO you should get an ordinal based lookup, same behaviour as with lists, arrays, etc. But then you have the other numeric types which implicitly cast to integer. But I don't see this as a huge problem. Numeric indexer = ordinal lookup. We'd just need an additional "itemWithKey" lookup method. – Trent May 20 '21 at 02:21
  • This is making way too complicated something that should be easy, and is hindering a useful functionality because of issues that exist only in the head of some. The `IDictionary` interface is very explicit and that's all that needs to be implemented. `OrderedDictionary` needs only to respect that, in the same way `Dictionary` and `SortedDictionary` already do. I don't see anyone complaining that `SortedDictionary` doesn't allow indexing with `int`s, so clearly that wouldn't be an issue to `OrderedDictionary` either. – Jorge Galvão Dec 13 '22 at 11:23
  • 1
    @JorgeGalvão: Much of what makes non-generic `OrderedDictionary` different from an ordinary `Dictionary` is the ability to retrieve the Nth elements, using *insertion order*. Adding elements to a SortedDictionary may change the meaning of e.g. the "third" element if elements are added that would sort before it, but in an OrderedDictionary, the third element will *always* be the third element. – supercat Dec 13 '22 at 16:32
  • @JorgeGalvão: On the other hand, it might have been useful to have the generic `Dictionary` class provide a constructor option that would disable the ability to delete items, but guarantee insertion-order enumeration, and/or support one-write-multi-read thread-safety (a `Dictionary` initially stores items in an array, in the order of addition, but reuses slots after items are deleted). – supercat Dec 13 '22 at 18:16
  • @supercat I reiterate what I have mentioned: This is a problem that exists only in the heads of some. The `this[int]` operator wouldn't exist, only the `this[K]` operator, so the only way there could be some confusion, is in the case where there is already confusion, which is when the `OrderedDictionary` contains `int`s as keys (and currently to use an int as a key one has to cast it to `object` first). – Jorge Galvão Jan 31 '23 at 12:55
  • @JorgeGalvão: Operand overloading rules require that every combination of operand types be uniquely resolvable for all combinations of generic type parameters. If a type contains an overload that accepts type `int` and an overload that accepts a generic type argument `T`, then `T` is required to have a constraint that would exclude `int`. – supercat Jan 31 '23 at 16:20
  • @supercat please refer to my initial comment for the reply to that, it seems that we are going in a loop. There wouldn't just exist a specific operand for `int`, and the collection would not be indexable by order, only by key of type `T` which could perfectly be an `int`. That would be a lot more advantageous than not existing such a collection (which is the status quo). – Jorge Galvão Jan 31 '23 at 21:01
  • @JorgeGalvão but that's the whole point of `OrderedDictionary` so you can index by the order of adding – Piotr Golacki Apr 03 '23 at 14:49
  • @PiotrGolacki: The guaranteed ordering of enumeration is a benefit of `OrderedDictionary` which could be supported by Dictionary as well. And thinking about it, one I think could have an `OrderedDictionary` which implements `IList` and must be cast to that type to access things be index. – supercat Apr 03 '23 at 15:14
  • @PiotrGolacki you'd iterate through the dictionary as you iterate through any other dictionary. You don't need indexing by order to do that. A getAt(i) method could be added and further Linq extensions could of course be provided, but even without those I once again repeat what I've been saying: not existing such a class is worst than it existing but not implementing the index[int] operator. – Jorge Galvão Apr 05 '23 at 08:49
  • @JorgeGalvão: For those purposes, I think I'd prefer to have a constructor option for a `Dictionary` which would make it behave as an append-only dictionary that is automatically one-writer multi-reader thread-safe, and whose `GetEnumerator` method would behave as a snapshot, so any code that uses `Dictionary` in that fashion would interoperate with code that needs the aforementioned features. – supercat Apr 05 '23 at 15:10
  • @supercat you are talking about s completely different type of container, not a Dictionary (so am I by the way, an OrderedDictionary in my case). I would not like the container to have to be append only, that is too restrictive. I literally just propose to have the exact same functionality that already exists in the non generic OrderedDictionary, just typed, and without the indexing by element position (which as pointed out would be incompatible with a generic version). – Jorge Galvão Apr 06 '23 at 21:05
  • @JorgeGalvão: Many applications that use `Dictionary` will never remove any keys from the dictionary during its lifetime, and the only difficult aspects of making a `Dictionary` single-writer multi-reader thread-safe involve deletions. – supercat Apr 06 '23 at 21:09
  • @supercat I'm not against such container, I'm just saying thats a completely different thing, almost not related to the topic ^_^ – Jorge Galvão Apr 06 '23 at 21:10
17

For what it's worth, here is how I solved it:

   public class PairList<TKey, TValue> : List<KeyValuePair<TKey, TValue>> {
        Dictionary<TKey, int> itsIndex = new Dictionary<TKey, int>();

        public void Add(TKey key, TValue value) {
            Add(new KeyValuePair<TKey, TValue>(key, value));
            itsIndex.Add(key, Count-1);
        }

        public TValue Get(TKey key) {
            var idx = itsIndex[key];
            return this[idx].Value;
        }
    }

It can be initialized like this:

var pairList = new PairList<string, string>
    {
        { "pitcher", "Ken" },
        { "catcher", "Brad"},
        { "left fielder", "Stan"},
    };

and accessed like this:

foreach (var pair in pairList)
{
    Console.WriteLine("position: {0}, player: {1}",
        pair.Key, pair.Value);
}

// Guaranteed to print in the order of initialization
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
JoelFan
  • 37,465
  • 35
  • 132
  • 205
  • 3
    Thanks! I hadn't realised that collection initialisers were just special syntax for `Add` methods. – Sam Oct 19 '12 at 02:56
  • 11
    This isn't a dictionary. Dictionary stands for *keyed indexing* and *no duplicate keys*. – nawfal Oct 31 '13 at 07:10
  • yet if you happen to not need indexing by key (which isn't too hard to add) and dupliacte keys this comes in handy – stijn May 16 '14 at 16:06
  • Very useful class! Just what I needed! – Joe Gayetty Dec 10 '15 at 20:17
  • 1
    You have a problem is code calls `pairList.Add(new KeyValuePair())` (i.e. the method on the `List` class). In that case, the `itsIndex` dictionary is not updated, and now the list and the dictionary are out of sync. Could hide the `List.Add` method by create a `new` `PairList.Add(KeyValuePair)` method, or could use composition instead of inheritance and just implement all the `List` methods again...a lot more code... – neizan Nov 09 '17 at 18:27
  • I think `override` would be better than `new`... but yes, that is a worthwhile addition – JoelFan Nov 09 '17 at 19:39
  • 1
    @nawfal, to address your concern, one could just add a check like: `if (itsindex.Contains(key)) return;` to the beginning of `Add` to prevent duplicates – JoelFan Nov 09 '17 at 19:42
7

For a lot of purposes I've found one can get by with a List<KeyValuePair<K, V>>. (Not if you need it to extend Dictionary, obviously, and not if you need better than O(n) key-value lookup.)

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
David Moles
  • 48,006
  • 27
  • 136
  • 235
  • Just came to the same conclusion myself! – Peter Mar 31 '11 at 08:31
  • But that has an awkward initialization syntax... check out my answer – JoelFan Dec 21 '11 at 17:25
  • And you don't get the convenient `.Values` and `.Keys` properties. – Pat Jul 30 '12 at 19:52
  • 1
    As I said, "for a lot of purposes." – David Moles Jul 30 '12 at 23:01
  • 2
    You could also use a `Tuple` if they don't have a key-value relationship. – cdmckay May 08 '13 at 15:00
  • 2
    What is the point of having key value pairs if you can't index by key? – DCShannon Jul 30 '15 at 01:56
  • 1
    @DCShannon You can still map keys to values, you just can't look them up any faster than O(n) (or deal automatically with duplicate keys). For small values of n that's sometimes good enough, especially in situations where you're commonly iterating through all the keys anyway. – David Moles Aug 02 '15 at 18:56
  • @DCShannon You can't. This is not a good replacement, in fact it's terrible. – John Stock Nov 18 '21 at 10:02
  • @JohnStock It's a fine replacement if you just need to associate a small number of known-to-be-unique keys with values, and you're commonly iterating over most or all of the list. It's a terrible replacement in the general case where you need random-access lookup, enforced key uniqueness, and the other goodies you get from a proper `Dictionary` implementation. – David Moles Nov 18 '21 at 17:54
7

For those looking for an "official" package option in NuGet, an implementation of a generic OrderedDictionary has been accepted into .NET CoreFX Lab. If all goes well, the type will eventually be approved and integrated to the main .NET CoreFX repo.

There is a possibility that this implementation will be rejected.

The committed implementation can be referenced here https://github.com/dotnet/corefxlab/blob/57be99a176421992e29009701a99a370983329a6/src/Microsoft.Experimental.Collections/Microsoft/Collections/Extensions/OrderedDictionary.cs

The NuGet package that definitively has this type available for use can be found here https://www.nuget.org/packages/Microsoft.Experimental.Collections/1.0.6-e190117-3

Or you can install the package within Visual Studio. Browse for the package "Microsoft.Experimental.Collections" and make sure the "Include prerelease" checkbox is selected.

Will update this post if and when the type is made officially available.

charlie
  • 4,612
  • 1
  • 22
  • 18
  • Can you estimate when it will be released? – mvorisek May 25 '19 at 16:52
  • I am taking no part in the development of this library so unfortunately I have no idea. It's likely that it will be a built-in framework collection if it ever gets "approved". – charlie Jul 01 '19 at 04:44
  • 2
    Unfortunately, this package has been abandoned. The [Microsoft.Experimental.Collections](https://www.nuget.org/packages/Microsoft.Experimental.Collections) NuGet package has been deprecated with no replacement, its GitHub repository ([dotnet/corefxlab](https://github.com/dotnet/corefxlab)) has been archived and there's no trace of `Microsoft.Experimental.Collections` in its replacement repository ([dotnet/runtimelab](https://github.com/dotnet/runtimelab)): [What happened to Microsoft.Experimental.Collections?](https://github.com/dotnet/runtimelab/issues/1881) – 0xced Dec 07 '22 at 09:43
  • 2
    After losing myself in the rabbit hole that is the messy Microsoft Experimental Collections legacy, I think I found the latest issue that is tracking this: https://github.com/dotnet/runtime/issues/24826. It is sadly still open with no priority assigned. – alelom Apr 05 '23 at 08:40
  • @alelom thanks for tracking that down. Utterly ridiculous that such a simple thing -- with a million-and-one implementations ready to go -- is simply treated as irrelevant. – McGuireV10 Sep 02 '23 at 22:42
6

Right, it's an unfortunate omission. I miss Python's OrderedDict

A dictionary that remembers the order that keys were first inserted. If a new entry overwrites an existing entry, the original insertion position is left unchanged. Deleting an entry and reinserting it will move it to the end.

So I wrote my own OrderedDictionary<K,V> class in C#. How does it work? It maintains two collections - a vanilla unordered dictionary and an ordered list of keys. With this solution, the standard dictionary operations keep their fast complexities, and look up by index is fast too.

https://gist.github.com/hickford/5137384

Here's the interface

/// <summary>
/// A dictionary that remembers the order that keys were first inserted. If a new entry overwrites an existing entry, the original insertion position is left unchanged. Deleting an entry and reinserting it will move it to the end.
/// </summary>
/// <typeparam name="TKey">The type of keys</typeparam>
/// <typeparam name="TValue">The type of values</typeparam>
public interface IOrderedDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    /// <summary>
    /// The value of the element at the given index.
    /// </summary>
    TValue this[int index] { get; set; }

    /// <summary>
    /// Find the position of an element by key. Returns -1 if the dictionary does not contain an element with the given key.
    /// </summary>
    int IndexOf(TKey key);

    /// <summary>
    /// Insert an element at the given index.
    /// </summary>
    void Insert(int index, TKey key, TValue value);

    /// <summary>
    /// Remove the element at the given index.
    /// </summary>
    void RemoveAt(int index);
}
Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
5

There is SortedDictionary<TKey, TValue>. Although semantically close, I am not claiming it's the same as OrderedDictionary simply because they are not. Even from performance characteristics. However the very interesting and quite important difference between Dictionary<TKey, TValue> (and to that extent OrderedDictionary and implementations provided in answers) and SortedDictionary is that the latter is using binary tree underneath. This is critical distinction because it makes the class immune to memory constraints applied to generic class. See this thread about OutOfMemoryExceptions thrown when generic class is used for handling large set of key-value pairs.

How to figure out the max value for capacity parameter passed to Dictionary constructor to avoid OutOfMemoryException?

Community
  • 1
  • 1
Schultz9999
  • 8,717
  • 8
  • 48
  • 87
  • Is there a way to get keys or values of a `SortedDictionary` *in the order they were added*? That's what `OrderedDictionary` allows. Different concept than *sorted*. (I've made this mistake in the past; I thought supplying a Comparer to OrderedDictionary constructor would make it sorted, but all it does with the Comparer is determine key equality; e.g. string-insensitive comparer allows string-insensitive key lookup.) – ToolmakerSteve Jan 26 '18 at 17:55
3

As a follow up to the comment from @V.B. here's an accessible implementation of the System.Runtime.Collections.OrderedDictionary<,>. I was originally going to access it by reflection and provide it via a factory but the dll this is in does not seem to be very accessible at all so I just pulled the source itself.

One thing to note is the indexer here will not throw KeyNotFoundException. I absolutely hate that convention and that was the 1 liberty i took in this implementation. If that's important to you, just replace the line for return default(TValue);. Uses C# 6 (compatible with Visual Studio 2013)

/// <summary>
///     System.Collections.Specialized.OrderedDictionary is NOT generic.
///     This class is essentially a generic wrapper for OrderedDictionary.
/// </summary>
/// <remarks>
///     Indexer here will NOT throw KeyNotFoundException
/// </remarks>
public class OrderedDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary
{
    private readonly OrderedDictionary _privateDictionary;

    public OrderedDictionary()
    {
        _privateDictionary = new OrderedDictionary();
    }

    public OrderedDictionary(IDictionary<TKey, TValue> dictionary)
    {
        if (dictionary == null) return;

        _privateDictionary = new OrderedDictionary();

        foreach (var pair in dictionary)
        {
            _privateDictionary.Add(pair.Key, pair.Value);
        }
    }

    public bool IsReadOnly => false;
    public int Count => _privateDictionary.Count;
    int ICollection.Count => _privateDictionary.Count;
    object ICollection.SyncRoot => ((ICollection)_privateDictionary).SyncRoot;
    bool ICollection.IsSynchronized => ((ICollection)_privateDictionary).IsSynchronized;

    bool IDictionary.IsFixedSize => ((IDictionary)_privateDictionary).IsFixedSize;
    bool IDictionary.IsReadOnly => _privateDictionary.IsReadOnly;
    ICollection IDictionary.Keys => _privateDictionary.Keys;
    ICollection IDictionary.Values => _privateDictionary.Values;

    void IDictionary.Add(object key, object value)
    {
        _privateDictionary.Add(key, value);
    }

    void IDictionary.Clear()
    {
        _privateDictionary.Clear();
    }

    bool IDictionary.Contains(object key)
    {
        return _privateDictionary.Contains(key);
    }

    IDictionaryEnumerator IDictionary.GetEnumerator()
    {
        return _privateDictionary.GetEnumerator();
    }

    void IDictionary.Remove(object key)
    {
        _privateDictionary.Remove(key);
    }

    object IDictionary.this[object key]
    {
        get { return _privateDictionary[key]; }
        set { _privateDictionary[key] = value; }
    }

    void ICollection.CopyTo(Array array, int index)
    {
        _privateDictionary.CopyTo(array, index);
    }

    public TValue this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException(nameof(key));

            if (_privateDictionary.Contains(key))
            {
                return (TValue) _privateDictionary[key];
            }

            return default(TValue);
        }
        set
        {
            if (key == null) throw new ArgumentNullException(nameof(key));

            _privateDictionary[key] = value;
        }
    }

    public ICollection<TKey> Keys
    {
        get
        {
            var keys = new List<TKey>(_privateDictionary.Count);

            keys.AddRange(_privateDictionary.Keys.Cast<TKey>());

            return keys.AsReadOnly();
        }
    }

    public ICollection<TValue> Values
    {
        get
        {
            var values = new List<TValue>(_privateDictionary.Count);

            values.AddRange(_privateDictionary.Values.Cast<TValue>());

            return values.AsReadOnly();
        }
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        Add(item.Key, item.Value);
    }

    public void Add(TKey key, TValue value)
    {
        if (key == null) throw new ArgumentNullException(nameof(key));

        _privateDictionary.Add(key, value);
    }

    public void Clear()
    {
        _privateDictionary.Clear();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        if (item.Key == null || !_privateDictionary.Contains(item.Key))
        {
            return false;
        }

        return _privateDictionary[item.Key].Equals(item.Value);
    }

    public bool ContainsKey(TKey key)
    {
        if (key == null) throw new ArgumentNullException(nameof(key));

        return _privateDictionary.Contains(key);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        if (array == null) throw new ArgumentNullException(nameof(array));
        if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex));
        if (array.Rank > 1 || arrayIndex >= array.Length
                           || array.Length - arrayIndex < _privateDictionary.Count)
            throw new ArgumentException("Bad Copy ToArray", nameof(array));

        var index = arrayIndex;
        foreach (DictionaryEntry entry in _privateDictionary)
        {
            array[index] = 
                new KeyValuePair<TKey, TValue>((TKey) entry.Key, (TValue) entry.Value);
            index++;
        }
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        foreach (DictionaryEntry entry in _privateDictionary)
        {
            yield return 
                new KeyValuePair<TKey, TValue>((TKey) entry.Key, (TValue) entry.Value);
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        if (false == Contains(item)) return false;

        _privateDictionary.Remove(item.Key);

        return true;
    }

    public bool Remove(TKey key)
    {
        if (key == null) throw new ArgumentNullException(nameof(key));

        if (false == _privateDictionary.Contains(key)) return false;

        _privateDictionary.Remove(key);

        return true;
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        if (key == null) throw new ArgumentNullException(nameof(key));

        var keyExists = _privateDictionary.Contains(key);
        value = keyExists ? (TValue) _privateDictionary[key] : default(TValue);

        return keyExists;
    }
}

Pull requests/discussion accepted on GitHub

Community
  • 1
  • 1
Chris Marisic
  • 32,487
  • 24
  • 164
  • 258
1

I implemented a generic OrderedDictionary<TKey, TValue> by wraping around SortedList<TKey, TValue> and adding a private Dictionary<TKey, int> _order. Then I created an internal implementation of Comparer<TKey>, passing a reference to the _order dictionary. Then I use this comparer for the internal SortedList. This class keeps the order of elements passed to the constructor and order of additions.

This implementation has almost the same big O characteristics as SortedList<TKey, TValue> since adding and removing to _order is O(1). Each element will take (according to the book 'C# 4 in a Nutshell', p. 292, table 7-1) additional memory space of 22 (overhead) + 4 (int order) + TKey size (let's assume 8) = 34. Together with SortedList<TKey, TValue>'s overhead of two bytes, the total overhead is 36 bytes, while the same book says that non-generic OrderedDictionary has an overhead of 59 bytes.

If I pass sorted=true to constructor, then _order is not used at all, the OrderedDictionary<TKey, TValue> is exactly SortedList<TKey, TValue> with minor overhead for wrapping, if at all meaningful.

I am going to store not-so-many large reference objects in the OrderedDictionary<TKey, TValue>, so for me this ca. 36 bytes overhead is tolerable.

The main code is below. The complete updated code is on this gist.

public class OrderedList<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary
{
    private readonly Dictionary<TKey, int> _order;
    private readonly SortedList<TKey, TValue> _internalList;

    private readonly bool _sorted;
    private readonly OrderComparer _comparer;

    public OrderedList(IDictionary<TKey, TValue> dictionary, bool sorted = false)
    {
        _sorted = sorted;

        if (dictionary == null)
            dictionary = new Dictionary<TKey, TValue>();

        if (_sorted)
        {
            _internalList = new SortedList<TKey, TValue>(dictionary);
        }
        else
        {
            _order = new Dictionary<TKey, int>();
            _comparer = new OrderComparer(ref _order);
            _internalList = new SortedList<TKey, TValue>(_comparer);
            // Keep order of the IDictionary
            foreach (var kvp in dictionary)
            {
                Add(kvp);
            }
        }
    }

    public OrderedList(bool sorted = false)
        : this(null, sorted)
    {
    }

    private class OrderComparer : Comparer<TKey>
    {
        public Dictionary<TKey, int> Order { get; set; }

        public OrderComparer(ref Dictionary<TKey, int> order)
        {
            Order = order;
        }

        public override int Compare(TKey x, TKey y)
        {
            var xo = Order[x];
            var yo = Order[y];
            return xo.CompareTo(yo);
        }
    }

    private void ReOrder()
    {
        var i = 0;
        _order = _order.OrderBy(kvp => kvp.Value).ToDictionary(kvp => kvp.Key, kvp => i++);
        _comparer.Order = _order;
        _lastOrder = _order.Values.Max() + 1;
    }

    public void Add(TKey key, TValue value)
    {
        if (!_sorted)
        {
            _order.Add(key, _lastOrder);
            _lastOrder++;

            // Very rare event
            if (_lastOrder == int.MaxValue)
                ReOrder();
        }

        _internalList.Add(key, value);
    }

    public bool Remove(TKey key)
    {
        var result = _internalList.Remove(key);
        if (!_sorted)
            _order.Remove(key);
        return result;
    }

    // Other IDictionary<> + IDictionary members implementation wrapping around _internalList
    // ...
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
V.B.
  • 6,236
  • 1
  • 33
  • 56
  • There are at least four different usage cases I can see for something like `OrderedDictionary`, with regard to insertions or deletions: (1) There will never be any deletions; (2) there will be deletions, but what is important is that items enumerate in the order added; there is no need for access by index; (3) the numerical index of an item should (or at least may) remain constant, and no more than 2 billion items will be added during the lifetime of the collection, so if item #7 is removed, there will never again be an item #7; (4) an item's index should be its rank with respect to survivors. – supercat Jun 22 '13 at 18:17
  • 1
    Scenarios #1 could be handled by using an array in parallel with the `Dictionary`. Scenarios #2 and #3 could be handled by having each item maintain an index saying when it was added and links to items added before or after it. Scenario #4 is the only one which would seem like it shouldn't be able to achieve O(1) performance for operations in arbitrary sequence. Depending upon usage patterns, #4 may be helped by using various lazy update strategies (keep counts in a tree, and have changes to a node invalidate rather than update the node and its parents). – supercat Jun 22 '13 at 18:26
  • @V.B. I dont understand this solution at all. I went thru ur code in github. I'll point one by one. **Critical flaws**: 1. Your `GetEnumerator` function merely enumerates the `SortedList` which is in sort order, not insertion order. An ordered dictionary is not a sorted dictionary. The major feature of an ordered dictionary is that it maintains the added order when **enumerating**. Yours doesnt. This is what the whole question is about (and you havent included that code) – nawfal May 01 '14 at 13:53
  • 2. When you do `ToDictionary` (a structure that doesn't preserve insertion order) code is flawed. It may work today, but it is not guaranteed. You're relying on implementation detail. – nawfal May 01 '14 at 13:56
  • 1
    Internal SortedList has elements in insertion order due to use of custom comparer. It may be slow but your comment about enumeration is wrong. Show the tests about enumeration... – V.B. May 01 '14 at 14:02
  • 1
    What line with ToDictionary you are talking about? It doesn't affect internal list, but only order dictionary. – V.B. May 01 '14 at 14:10
  • As for speed, it would be interesting to compare it with KeyedCollection (to which I switched actually). I thing you should remove downvote, BTW – V.B. May 01 '14 at 14:10
  • 1
    @V.B. Apologies, I missed both. As such haven't tested it. Then the only problem would be with addition. Two dictionary lookups, as well as two insertions. I will cancel the downvote. – nawfal May 01 '14 at 14:14
  • I find this implementation quite confusing. Can you provide tests that demonstrate that the *original order of insertion* is returned, after insertion, deletion, sorting? [I've read the above comments back and forth, but given a non-obvious implementation, the burden of proof of correctness is on the implementor. You've said that SortedList maintains insertion order. I suggest including tests, from outside the class, that verify that. And show how to extract that original order.] – ToolmakerSteve Jan 26 '18 at 18:05
  • @ToolmakerSteve It's 5 yo snippet that doesn't exist anywhere anymore except in this answer. There must be some ready-to-use tested implementation On GitHub. – V.B. Jan 26 '18 at 22:17
1

This is not yet another version/solution of an OrderedDictionary<,> but an experiment I did testing each of 4 versions mentioned in the answers: of @Colonel Panic, @mattmc3, @V.B. @Chris Marisic. It is meant as a feedback. Well, partial because I have to admit I haven't dissected the code, so there may be differences in functionality or safety checks. But still, I thought feedback would be useful on their performance. And as you'll see time can get from a couple of milliseconds to a quarter of hour. Then I scribbled a naive minimal version with 2 lists of key and value class objects with O(n) search just to see the magnitude of the benefit of O(1) access.

Testbed is Microsoft Visual Studio Community 2019 with Unity 3D, 4 consecutive times for each test and the code that I wanted to replicate a real-ish scenario in is

using System.Text;
using UnityEngine;

public class TessyOne : MonoBehaviour
{
    public const int iterations = 50000;
    private System.Diagnostics.Stopwatch stopwatch;
    private System.Random random;
    public float stopwatchDuration;

    public class Ala
    {
        public int inta;
        public float fla;
        public string stra;
        public Ben bena;

        public Ala(int i, float f, string s, Ben b)
        {
            inta = i; fla = f; stra = s; bena = b;
        }
    }

    public class Ben
    {
        public int inte;
        public float fle;
        public string stre;

        public Ben(int i, float f, string s)
        {
            inte = i; fle = f; stre = s;
        }
    }

    //public Naive.OrderedDictionary<Ala, Ben> alasToBens = new Naive.OrderedDictionary<Ala, Ben>();
    //public Hickford.OrderedDictionary<Ala, Ben> alasToBens = new Hickford.OrderedDictionary<Ala, Ben>();
    //public Mattmc3.OrderedDictionary<Ala, Ben> alasToBens = new Mattmc3.OrderedDictionary<Ala, Ben>();
    public Marisic.OrderedDictionary<Ala, Ben> alasToBens = new Marisic.OrderedDictionary<Ala, Ben>();
    //public VB.OrderedList<Ala, Ben> alasToBens = new VB.OrderedList<Ala, Ben>(null, false);

    Ala[] alarray = new Ala[iterations];
    Ben[] berray = new Ben[iterations];

    // This is the entry point of the application 
    private void Start()
    {
        stopwatch = new System.Diagnostics.Stopwatch();
        random = new System.Random(2020);

        for(int i = 0; i < iterations; ++i)
        {
            berray[i] = new Ben(random.Next(),
                                (float)random.NextDouble(),
                                MakeRandomString((ushort)random.Next(1, 10)));
            alarray[i] = new Ala(random.Next(),
                                 (float)random.NextDouble(),
                                  MakeRandomString((ushort)random.Next(1, 10)),
                                  berray[i]);
            // uncomment for testing ContainsKey() and Remove(), comment for Add()
            alasToBens.Add(alarray[i], berray[i]);
        }
    
        stopwatch.Start();
        for(int i = iterations - 1; i > -1; --i)
        {
            //alasToBens.Add(alarray[i], berray[i]);
            //alasToBens.ContainsKey(alarray[i]);
            alasToBens.Remove(alarray[i]);
        }
        stopwatch.Stop();
        stopwatchDuration = stopwatch.ElapsedMilliseconds;
    }

    public string MakeRandomString(ushort length)
    {
        StringBuilder sb = new StringBuilder();
        for(ushort u = 0; u < length; ++u)
        {
            sb.Append((char)Random.Range(33, 126)); // regular ASCII chars
        }
        return sb.ToString();
    }
}

Note that the tests are for worst case scenarios in the case of naive version at least, as it iterates through the collection from index 0 through iterations and searching is done from end to start. I measured Add(), ContainsKey() and Remove() in milliseconds for a dictionary of 50000 entries. Results:

+----------+----------------+----------------+--------------------------------+
|    ms    |     Add()      | ContainsKey()  |            Remove()            |
+----------+----------------+----------------+--------------------------------+
| Hickford |     7, 8, 7, 8 |     2, 2, 3, 2 |         7400, 7503, 7419, 7421 |
| Mattmc3  | 23, 24, 24, 23 |     3, 3, 3, 3 | 890404, 913465, 875387, 877792 |
| Marisic  | 27, 28, 28, 27 |     4, 4, 4, 4 |     27401, 27627, 27341, 27349 |
| V.B.     | 76, 76, 75, 75 | 59, 60, 60, 60 |                 66, 67, 67, 67 |
|          |                |                |                                |
| Naive    |   19651, 19761 |   25335, 25416 |                   25259, 25306 |
+----------+----------------+----------------+--------------------------------+
mireazma
  • 526
  • 1
  • 8
  • 20
  • 1
    This was flagged as _Not an Answer_. That's absolutely correct. But it also provides _useful_ information that should benefit future readers, but which wouldn't fit well in a comment. For that reason, I'm inclined to let it stand. – Jeremy Caney Oct 05 '20 at 20:16