diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 515366d6c983f..3625b253780b1 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -116,6 +116,7 @@ public OrderedDictionary(System.Collections.Generic.IEnumerable? comparer) { } public OrderedDictionary(int capacity) { } public OrderedDictionary(int capacity, System.Collections.Generic.IEqualityComparer? comparer) { } + public int Capacity { get { throw null; } } public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } public int Count { get { throw null; } } public TValue this[TKey key] { get { throw null; } set { } } @@ -142,6 +143,7 @@ public void Add(TKey key, TValue value) { } public void Clear() { } public bool ContainsKey(TKey key) { throw null; } public bool ContainsValue(TValue value) { throw null; } + public int EnsureCapacity(int capacity) { throw null; } public System.Collections.Generic.KeyValuePair GetAt(int index) { throw null; } public System.Collections.Generic.OrderedDictionary.Enumerator GetEnumerator() { throw null; } public int IndexOf(TKey key) { throw null; } @@ -170,6 +172,8 @@ void System.Collections.IDictionary.Remove(object key) { } void System.Collections.IList.Insert(int index, object? value) { } void System.Collections.IList.Remove(object? value) { } public void TrimExcess() { } + public void TrimExcess(int capacity) { } + public bool TryAdd(TKey key, TValue value) { throw null; } public bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } public partial struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IDictionaryEnumerator, System.Collections.IEnumerator, System.IDisposable { diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj index 63264076c8708..ba91a4f329133 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -10,14 +10,11 @@ - - - - + + + + + @@ -32,14 +29,10 @@ - - - - + + + + diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs index 52b80e3edbacb..284dfcf8b393e 100644 --- a/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs +++ b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs @@ -8,43 +8,15 @@ namespace System.Collections.Generic { - // Implementation notes: - // --------------------- - // Ideally, all of the following would be O(1): - // - Lookup by key - // - Indexing by position - // - Adding - // - Inserting - // - Removing - // - // There's not a good way to achieve all of those, e.g. - // - A map for lookups with an array list achieves O(1) lookups, indexing, and adding, but O(N) insert and removal. - // - A map for lookups with a linked list achieves O(1) lookups, adding, removal, and insert, but O(N) indexing. - // - // There are also layout and memory consumption tradeoffs. For example, a map to nodes containing keys and values - // means lots of indirections as part of enumerating. Alternatively, the keys and values can be duplicated in both - // a map and a list, leading to larger memory consumption, but optimizing for speed of data access. Or the keys - // and values can be stored in the map with only the key stored in the list. - // - // This implementation currently employs the simple strategy of using both a dictionary and a list, with the - // dictionary as the source of truth for the key/value pairs, and the list storing just the keys in order. This - // provides O(1) lookups, adding, and indexing, with O(N) insert and removal. Keys are duplicated in memory, - // but lookups are optimized to be simple dictionary accesses. Enumeration is O(N), and involves enumerating - // the list for order and performing a lookup on each element to get its value. This is the same approach taken - // by the non-generic OrderedDictionary and thus keeps algorithmic complexity consistent for someone upgrading - // from the non-generic to generic types. It's also important for consumption via the interfaces, in particular - // I{ReadOnly}List, where it's common to iterate through a list with an indexer, and if indexing were O(N) - // instead of O(1), it would turn such loops into O(N^2) instead of O(N). - // - // Currently the implementation is optimized for simplicity and correctness, choosing to wrap a Dictionary<> - // and a List<> rather than implementing a custom data structure. They could be flattened to partially - // deduped in the future if the extra overhead is deemed prohibitive. - /// /// Represents a collection of key/value pairs that are accessible by the key or index. /// /// The type of the keys in the dictionary. /// The type of the values in the dictionary. + /// + /// Operations on the collection have algorithmic complexities that are similar to that of the + /// class, except with lookups by key similar in complexity to that of . + /// [DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))] [DebuggerDisplay("Count = {Count}")] public class OrderedDictionary : @@ -52,10 +24,22 @@ public class OrderedDictionary : IList>, IReadOnlyList>, IList where TKey : notnull { - /// Store for the key/value pairs in the dictionary. - private readonly Dictionary _dictionary; - /// List storing the keys in order. - private readonly List _list; + /// The comparer used by the collection. May be null if the default comparer is used. + private IEqualityComparer? _comparer; + /// Indexes into for the start of chains; indices are 1-based. + private int[]? _buckets; + /// Ordered entries in the dictionary. + /// + /// Unlike , removed entries are actually removed rather than left as holes + /// that can be filled in by subsequent additions. This is done to retain ordering. + /// + private Entry[]? _entries; + /// The number of items in the collection. + private int _count; + /// Version number used to invalidate an enumerator. + private int _version; + /// Multiplier used on 64-bit to enable faster % operations. + private ulong _fastModMultiplier; /// Lazily-initialized wrapper collection that serves up only the keys, in order. private KeyCollection? _keys; @@ -66,10 +50,8 @@ public class OrderedDictionary : /// Initializes a new instance of the class that is empty, /// has the default initial capacity, and uses the default equality comparer for the key type. /// - public OrderedDictionary() + public OrderedDictionary() : this(0, null) { - _dictionary = []; - _list = []; } /// @@ -78,10 +60,8 @@ public OrderedDictionary() /// /// The initial number of elements that the can contain. /// capacity is less than 0. - public OrderedDictionary(int capacity) + public OrderedDictionary(int capacity) : this(capacity, null) { - _dictionary = new(capacity); - _list = new(capacity); } /// @@ -92,10 +72,8 @@ public OrderedDictionary(int capacity) /// The implementation to use when comparing keys, /// or null to use the default for the type of the key. /// - public OrderedDictionary(IEqualityComparer? comparer) + public OrderedDictionary(IEqualityComparer? comparer) : this(0, comparer) { - _dictionary = new(comparer); - _list = []; } /// @@ -110,8 +88,39 @@ public OrderedDictionary(IEqualityComparer? comparer) /// capacity is less than 0. public OrderedDictionary(int capacity, IEqualityComparer? comparer) { - _dictionary = new(capacity, comparer); - _list = new(capacity); + ArgumentOutOfRangeException.ThrowIfNegative(capacity); + + if (capacity > 0) + { + EnsureBucketsAndEntriesInitialized(capacity); + } + + // Initialize the comparer: + // - Strings: Special-case EqualityComparer.Default, StringComparer.Ordinal, and + // StringComparer.OrdinalIgnoreCase. We start with a non-randomized comparer for improved throughput, + // falling back to a randomized comparer if the hash buckets become sufficiently unbalanced to cause + // more collisions than a preset threshold. + // - Other reference types: we always want to store a comparer instance, either the one provided, + // or if one wasn't provided, the default (accessing EqualityComparer.Default + // with shared generics on every dictionary access can add measurable overhead). + // - Value types: if no comparer is provided, or if the default is provided, we'd prefer to use + // EqualityComparer.Default.Equals on every use, enabling the JIT to + // devirtualize and possibly inline the operation. + if (!typeof(TKey).IsValueType) + { + _comparer = comparer ?? EqualityComparer.Default; + + if (typeof(TKey) == typeof(string) && + NonRandomizedStringEqualityComparer.GetStringComparer(_comparer!) is IEqualityComparer stringComparer) + { + _comparer = (IEqualityComparer)stringComparer; + } + } + else if (comparer is not null && // first check for null to avoid forcing default comparer instantiation unnecessarily + comparer != EqualityComparer.Default) + { + _comparer = comparer; + } } /// @@ -123,16 +132,8 @@ public OrderedDictionary(int capacity, IEqualityComparer? comparer) /// The initial order of the elements in the new collection is the order the elements are enumerated from the supplied dictionary. /// /// is null. - public OrderedDictionary(IDictionary dictionary) + public OrderedDictionary(IDictionary dictionary) : this(dictionary, null) { - ArgumentNullException.ThrowIfNull(dictionary); - - int capacity = dictionary.Count; - - _dictionary = new(capacity); - _list = new(capacity); - - AddRange(dictionary); } /// @@ -148,14 +149,11 @@ public OrderedDictionary(IDictionary dictionary) /// or null to use the default for the type of the key. /// /// is null. - public OrderedDictionary(IDictionary dictionary, IEqualityComparer? comparer) + public OrderedDictionary(IDictionary dictionary, IEqualityComparer? comparer) : + this(dictionary?.Count ?? 0, comparer) { ArgumentNullException.ThrowIfNull(dictionary); - int capacity = dictionary.Count; - _dictionary = new(capacity, comparer); - _list = new(capacity); - AddRange(dictionary); } @@ -168,15 +166,8 @@ public OrderedDictionary(IDictionary dictionary, IEqualityComparer /// The initial order of the elements in the new collection is the order the elements are enumerated from the supplied collection. /// /// is null. - public OrderedDictionary(IEnumerable> collection) + public OrderedDictionary(IEnumerable> collection) : this(collection, null) { - ArgumentNullException.ThrowIfNull(collection); - - int capacity = collection is ICollection> c ? c.Count : 0; - _dictionary = new(capacity); - _list = new(capacity); - - AddRange(collection); } /// @@ -192,22 +183,48 @@ public OrderedDictionary(IEnumerable> collection) /// or null to use the default for the type of the key. /// /// is null. - public OrderedDictionary(IEnumerable> collection, IEqualityComparer? comparer) + public OrderedDictionary(IEnumerable> collection, IEqualityComparer? comparer) : + this((collection as ICollection>)?.Count ?? 0, comparer) { ArgumentNullException.ThrowIfNull(collection); - int capacity = collection is ICollection> c ? c.Count : 0; - _dictionary = new(capacity, comparer); - _list = new(capacity); - AddRange(collection); } + /// Initializes the /. + /// + [MemberNotNull(nameof(_buckets))] + [MemberNotNull(nameof(_entries))] + private void EnsureBucketsAndEntriesInitialized(int capacity) + { + Resize(HashHelpers.GetPrime(capacity)); + } + + /// Gets the total number of key/value pairs the internal data structure can hold without resizing. + public int Capacity => _entries?.Length ?? 0; + /// Gets the that is used to determine equality of keys for the dictionary. - public IEqualityComparer Comparer => _dictionary.Comparer; + public IEqualityComparer Comparer + { + get + { + IEqualityComparer? comparer = _comparer; + + // If the key is a string, we may have substituted a non-randomized comparer during construction. + // If we did, fish out and return the actual comparer that had been provided. + if (typeof(TKey) == typeof(string) && + (comparer as NonRandomizedStringEqualityComparer)?.GetUnderlyingEqualityComparer() is IEqualityComparer ec) + { + return ec; + } + + // Otherwise, return whatever comparer we have, or the default if none was provided. + return comparer ?? EqualityComparer.Default; + } + } /// Gets the number of key/value pairs contained in the . - public int Count => _dictionary.Count; + public int Count => _count; /// bool ICollection>.IsReadOnly => false; @@ -252,7 +269,7 @@ public OrderedDictionary(IEnumerable> collection, IEq bool ICollection.IsSynchronized => false; /// - object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + object ICollection.SyncRoot => this; /// object? IList.this[int index] @@ -331,17 +348,107 @@ KeyValuePair IList>.this[int index] /// Setting the value of an existing key does not impact its order in the collection. public TValue this[TKey key] { - get => _dictionary[key]; + get + { + if (!TryGetValue(key, out TValue? value)) + { + ThrowHelper.ThrowKeyNotFound(key); + } + + return value; + } set { - ref TValue? valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, key, out bool keyExists); + ArgumentNullException.ThrowIfNull(key); + + bool modified = TryInsert(-1, key, value, InsertionBehavior.OverwriteExisting); + Debug.Assert(modified); + } + } - valueRef = value; - if (!keyExists) + /// Insert the key/value pair at the specified index. + /// The index at which to insert the pair, or -1 to append. + /// The key to insert. + /// The value to insert. + /// + /// The behavior controlling insertion behavior with respect to key duplication: + /// - IgnoreInsertion: Immediately ends the operation, returning false, if the key already exists, e.g. TryAdd(key, value) + /// - OverwriteExisting: If the key already exists, overwrites its value with the specified value, e.g. this[key] = value + /// - ThrowOnExisting: If the key already exists, throws an exception, e.g. Add(key, value) + /// + /// true if the collection was updated; otherwise, false. + private bool TryInsert(int index, TKey key, TValue value, InsertionBehavior behavior) + { + // Search for the key in the dictionary. + uint hashCode = 0, collisionCount = 0; + int i = IndexOf(key, ref hashCode, ref collisionCount); + + // Handle the case where the key already exists, based on the requested behavior. + if (i >= 0) + { + Debug.Assert(_entries is not null); + + switch (behavior) { - _list.Add(key); + case InsertionBehavior.OverwriteExisting: + _entries[i].Value = value; + return true; + + case InsertionBehavior.ThrowOnExisting: + ThrowHelper.ThrowDuplicateKey(key); + break; + + default: + return false; } } + + // The key doesn't exist. If a non-negative index was provided, that is the desired index at which to insert, + // which should have already been validated by the caller. If negative, we're appending. + if (index < 0) + { + index = _count; + } + Debug.Assert(index <= _count); + + // Ensure the collection has been initialized. + if (_buckets is null) + { + EnsureBucketsAndEntriesInitialized(0); + } + + // As we just initialized the collection, _entries must be non-null. + Entry[]? entries = _entries; + Debug.Assert(entries is not null); + + // Grow capacity if necessary to accomodate the extra entry. + if (entries.Length == _count) + { + Resize(HashHelpers.ExpandPrime(entries.Length)); + entries = _entries; + } + + // The _entries array is ordered, so we need to insert the new entry at the specified index. That means + // not only shifting up all elements at that index and higher, but also updating the buckets and chains + // to record the newly updated indices. + for (i = _count - 1; i >= index; --i) + { + entries[i + 1] = entries[i]; + UpdateBucketIndex(i, shiftAmount: 1); + } + + // Store the new key/value pair. + ref Entry entry = ref entries[index]; + entry.HashCode = hashCode; + entry.Key = key; + entry.Value = value; + PushEntryIntoBucket(ref entry, index); + _count++; + _version++; + + RehashIfNecessary(collisionCount, entries); + + return true; } /// Adds the specified key and value to the dictionary. @@ -351,8 +458,21 @@ public TValue this[TKey key] /// An element with the same key already exists in the . public void Add(TKey key, TValue value) { - _dictionary.Add(key, value); - _list.Add(key); + ArgumentNullException.ThrowIfNull(key); + + TryInsert(-1, key, value, InsertionBehavior.ThrowOnExisting); + } + + /// Adds the specified key and value to the dictionary if the key doesn't already exist. + /// The key of the element to add. + /// The value of the element to add. The value can be null for reference types. + /// key is null. + /// true if the key didn't exist and the key and value were added to the dictionary; otherwise, false. + public bool TryAdd(TKey key, TValue value) + { + ArgumentNullException.ThrowIfNull(key); + + return TryInsert(-1, key, value, InsertionBehavior.IgnoreInsertion); } /// Adds each element of the enumerable to the dictionary. @@ -379,20 +499,59 @@ private void AddRange(IEnumerable> collection) /// Removes all keys and values from the . public void Clear() { - _dictionary.Clear(); - _list.Clear(); + if (_buckets is not null && _count != 0) + { + Debug.Assert(_entries is not null); + + Array.Clear(_buckets, 0, _buckets.Length); + Array.Clear(_entries, 0, _count); + _count = 0; + _version++; + } } /// Determines whether the contains the specified key. /// The key to locate in the . /// true if the contains an element with the specified key; otherwise, false. - public bool ContainsKey(TKey key) => - _dictionary.ContainsKey(key); + public bool ContainsKey(TKey key) => IndexOf(key) >= 0; /// Determines whether the contains a specific value. /// The value to locate in the . The value can be null for reference types. /// true if the contains an element with the specified value; otherwise, false. - public bool ContainsValue(TValue value) => _dictionary.ContainsValue(value); + public bool ContainsValue(TValue value) + { + int count = _count; + + Entry[]? entries = _entries; + if (entries is null) + { + return false; + } + + if (typeof(TValue).IsValueType) + { + for (int i = 0; i < count; i++) + { + if (EqualityComparer.Default.Equals(value, entries[i].Value)) + { + return true; + } + } + } + else + { + EqualityComparer comparer = EqualityComparer.Default; + for (int i = 0; i < count; i++) + { + if (comparer.Equals(value, entries[i].Value)) + { + return true; + } + } + } + + return false; + } /// Gets the key/value pair at the specified index. /// The zero-based index of the pair to get. @@ -400,8 +559,15 @@ public bool ContainsKey(TKey key) => /// is less than 0 or greater than or equal to . public KeyValuePair GetAt(int index) { - TKey key = _list[index]; - return new(key, _dictionary[key]); + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexOutOfRange(); + } + + Debug.Assert(_entries is not null, "count must be positive, which means we must have entries"); + + ref Entry e = ref _entries[index]; + return KeyValuePair.Create(e.Key, e.Value); } /// Determines the index of a specific key in the . @@ -412,7 +578,105 @@ public int IndexOf(TKey key) { ArgumentNullException.ThrowIfNull(key); - return _list.IndexOf(key); + uint _ = 0; + return IndexOf(key, ref _, ref _); + } + + private int IndexOf(TKey key, ref uint outHashCode, ref uint outCollisionCount) + { + Debug.Assert(key is not null, "Key nullness should have been validated by caller."); + + uint hashCode; + uint collisionCount = 0; + IEqualityComparer? comparer = _comparer; + + if (_buckets is null) + { + hashCode = (uint)(comparer?.GetHashCode(key) ?? key.GetHashCode()); + collisionCount = 0; + goto ReturnNotFound; + } + + int i = -1; + ref Entry entry = ref Unsafe.NullRef(); + + Entry[]? entries = _entries; + Debug.Assert(entries is not null, "expected entries to be is not null"); + + if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types + comparer is null) + { + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + + hashCode = (uint)key.GetHashCode(); + i = GetBucket(hashCode) - 1; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. + do + { + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length) + { + goto ReturnNotFound; + } + + entry = ref entries[i]; + if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Key, key)) + { + goto Return; + } + + i = entry.Next; + + collisionCount++; + } + while (collisionCount <= (uint)entries.Length); + + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + goto ConcurrentOperation; + } + else + { + Debug.Assert(comparer is not null); + hashCode = (uint)comparer.GetHashCode(key); + i = GetBucket(hashCode) - 1; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. + do + { + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length) + { + goto ReturnNotFound; + } + + entry = ref entries[i]; + if (entry.HashCode == hashCode && comparer.Equals(entry.Key, key)) + { + goto Return; + } + + i = entry.Next; + + collisionCount++; + } + while (collisionCount <= (uint)entries.Length); + + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + goto ConcurrentOperation; + } + + ReturnNotFound: + i = -1; + outCollisionCount = collisionCount; + goto Return; + + ConcurrentOperation: + // We examined more entries than are actually in the list, which means there's a cycle + // that's caused by erroneous concurrent use. + ThrowHelper.ThrowConcurrentOperation(); + + Return: + outHashCode = hashCode; + return i; } /// Inserts an item into the collection at the specified index. @@ -424,28 +688,20 @@ public int IndexOf(TKey key) /// is less than 0 or greater than . public void Insert(int index, TKey key, TValue value) { - if ((uint)index > (uint)_list.Count) + if ((uint)index > (uint)_count) { - throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); + ThrowHelper.ThrowIndexOutOfRange(); } - _dictionary.Add(key, value); - _list.Insert(index, key); + ArgumentNullException.ThrowIfNull(key); + + TryInsert(index, key, value, InsertionBehavior.ThrowOnExisting); } /// Removes the value with the specified key from the . /// The key of the element to remove. /// - public bool Remove(TKey key) - { - if (_dictionary.Remove(key)) - { - _list.Remove(key); - return true; - } - - return false; - } + public bool Remove(TKey key) => Remove(key, out _); /// Removes the value with the specified key from the and copies the element to the value parameter. /// The key of the element to remove. @@ -453,12 +709,22 @@ public bool Remove(TKey key) /// true if the element is successfully found and removed; otherwise, false. public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) { - if (_dictionary.Remove(key, out value)) + ArgumentNullException.ThrowIfNull(key); + + // Find the key. + int index = IndexOf(key); + if (index >= 0) { - _list.Remove(key); + // It exists. Remove it. + Debug.Assert(_entries is not null); + + value = _entries[index].Value; + RemoveAt(index); + return true; } + value = default; return false; } @@ -466,15 +732,42 @@ public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) /// The zero-based index of the item to remove. public void RemoveAt(int index) { - TKey key = _list[index]; - _list.RemoveAt(index); - _dictionary.Remove(key); + int count = _count; + if ((uint)index >= (uint)count) + { + ThrowHelper.ThrowIndexOutOfRange(); + } + + // Remove from the associated bucket chain the entry that lives at the specified index. + RemoveEntryFromBucket(index); + + // Shift down all entries above this one, and fix up the bucket chains to reflect the new indices. + Entry[]? entries = _entries; + Debug.Assert(entries is not null); + for (int i = index + 1; i < count; i++) + { + entries[i - 1] = entries[i]; + UpdateBucketIndex(i, shiftAmount: -1); + } + + entries[--_count] = default; + _version++; } /// Sets the value for the key at the specified index. /// The zero-based index of the element to get or set. /// The value to store at the specified index. - public void SetAt(int index, TValue value) => _dictionary[_list[index]] = value; + public void SetAt(int index, TValue value) + { + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexOutOfRange(); + } + + Debug.Assert(_entries is not null); + + _entries[index].Value = value; + } /// Sets the key/value pair at the specified index. /// The zero-based index of the element to get or set. @@ -483,24 +776,97 @@ public void RemoveAt(int index) /// public void SetAt(int index, TKey key, TValue value) { - TKey existing = _list[index]; + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexOutOfRange(); + } + + ArgumentNullException.ThrowIfNull(key); + + Debug.Assert(_entries is not null); + ref Entry e = ref _entries[index]; + + // If the key matches the one that's already in that slot, just update the value. + if (typeof(TKey).IsValueType && _comparer is null) + { + if (EqualityComparer.Default.Equals(key, e.Key)) + { + e.Value = value; + return; + } + } + else + { + Debug.Assert(_comparer is not null); + if (_comparer.Equals(key, e.Key)) + { + e.Value = value; + return; + } + } - if (_dictionary.ContainsKey(key)) + // The key doesn't match that index. If it exists elsewhere in the collection, fail. + uint _ = 0, collisionCount = 0; + if (IndexOf(key, ref _, ref collisionCount) >= 0) { - throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key), nameof(key)); + ThrowHelper.ThrowDuplicateKey(key); } - _dictionary.Remove(existing); - _dictionary.Add(key, value); + // The key doesn't exist in the collection. Update the key and value, but also update + // the bucket chains, as the new key may not hash to the same bucket as the old key + // (we could check for this, but in a properly balanced dictionary the chances should + // be low for a match, so it's not worth it). + RemoveEntryFromBucket(index); + e.Key = key; + e.Value = value; + PushEntryIntoBucket(ref e, index); + + _version++; + + RehashIfNecessary(collisionCount, _entries); + } + + /// Ensures that the dictionary can hold up to entries without resizing. + /// The desired minimum capacity of the dictionary. The actual capacity provided may be larger. + /// The new capacity of the dictionary. + /// is negative. + public int EnsureCapacity(int capacity) + { + ArgumentOutOfRangeException.ThrowIfNegative(capacity); + + if (Capacity < capacity) + { + if (_buckets is null) + { + EnsureBucketsAndEntriesInitialized(capacity); + } + else + { + Resize(HashHelpers.GetPrime(capacity)); + } - _list[index] = key; + _version++; + } + + return Capacity; } /// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries. - public void TrimExcess() + public void TrimExcess() => TrimExcess(_count); + + /// Sets the capacity of this dictionary to hold up a specified number of entries without resizing. + /// The desired capacity to which to shrink the dictionary. + /// is less than . + public void TrimExcess(int capacity) { - _dictionary.TrimExcess(); - _list.TrimExcess(); + ArgumentOutOfRangeException.ThrowIfLessThan(capacity, Count); + + int currentCapacity = _entries?.Length ?? 0; + capacity = HashHelpers.GetPrime(capacity); + if (capacity < currentCapacity) + { + Resize(capacity); + } } /// Gets the value associated with the specified key. @@ -510,17 +876,232 @@ public void TrimExcess() /// otherwise, the default value for the type of the value parameter. /// /// true if the contains an element with the specified key; otherwise, false. - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetValue(key, out value); + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + ArgumentNullException.ThrowIfNull(key); - /// Returns an enumerator that iterates through the . - /// A structure for the . - public Enumerator GetEnumerator() + // Find the key. + int index = IndexOf(key); + if (index >= 0) + { + // It exists. Return its value. + Debug.Assert(_entries is not null); + value = _entries[index].Value; + return true; + } + + value = default; + return false; + } + + /// Pushes the entry into its bucket. + /// + /// The bucket is a linked list by index into the array. + /// The new entry's is set to the bucket's current + /// head, and then the new entry is made the new head. + /// + private void PushEntryIntoBucket(ref Entry entry, int entryIndex) + { + ref int bucket = ref GetBucket(entry.HashCode); + entry.Next = bucket - 1; + bucket = entryIndex + 1; + } + + /// Removes an entry from its bucket. + private void RemoveEntryFromBucket(int entryIndex) { - AssertInvariants(); + // We're only calling this method if there's an entry to be removed, in which case + // entries must have been initialized. + Entry[]? entries = _entries; + Debug.Assert(entries is not null); - return new(this, useDictionaryEntry: false); + // Get the entry to be removed and the associated bucket. + Entry entry = entries[entryIndex]; + ref int bucket = ref GetBucket(entry.HashCode); + + if (bucket == entryIndex + 1) + { + // If the entry was at the head of its bucket list, to remove it from the list we + // simply need to update the next entry in the list to be the new head. + bucket = entry.Next + 1; + } + else + { + // The entry wasn't the head of the list. Walk the chain until we find the entry, + // updating the previous entry's Next to point to this entry's Next. + int i = bucket - 1; + int collisionCount = 0; + while (true) + { + ref Entry e = ref entries[i]; + if (e.Next == entryIndex) + { + e.Next = entry.Next; + return; + } + + i = e.Next; + + if (++collisionCount > entries.Length) + { + // We examined more entries than are actually in the list, which means there's a cycle + // that's caused by erroneous concurrent use. + ThrowHelper.ThrowConcurrentOperation(); + } + } + } } + /// + /// Updates the bucket chain containing the specified entry (by index) to shift indices + /// by the specified amount. + /// + /// The index of the target entry. + /// + /// 1 if this is part of an insert and the values are being shifted one higher. + /// -1 if this is part of a remove and the values are being shifted one lower. + /// + private void UpdateBucketIndex(int entryIndex, int shiftAmount) + { + Debug.Assert(shiftAmount is 1 or -1); + + Entry[]? entries = _entries; + Debug.Assert(entries is not null); + + Entry entry = entries[entryIndex]; + ref int bucket = ref GetBucket(entry.HashCode); + + if (bucket == entryIndex + 1) + { + // If the entry was at the head of its bucket list, the only thing that needs to be updated + // is the bucket head value itself, since no other entries' Next will be referencing this node. + bucket += shiftAmount; + } + else + { + // The entry wasn't the head of the list. Walk the chain until we find the entry, updating + // the previous entry's Next that's pointing to the target entry. + int i = bucket - 1; + int collisionCount = 0; + while (true) + { + ref Entry e = ref entries[i]; + if (e.Next == entryIndex) + { + e.Next += shiftAmount; + return; + } + + i = e.Next; + + if (++collisionCount > entries.Length) + { + // We examined more entries than are actually in the list, which means there's a cycle + // that's caused by erroneous concurrent use. + ThrowHelper.ThrowConcurrentOperation(); + } + } + } + } + + /// + /// Checks to see whether the collision count that occurred during lookup warrants upgrading to a non-randomized comparer, + /// and does so if necessary. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RehashIfNecessary(uint collisionCount, Entry[] entries) + { + // If we exceeded the hash collision threshold and we're using a randomized comparer, rehash. + // This is only ever done for string keys, so we can optimize it all away for value type keys. + if (!typeof(TKey).IsValueType && + collisionCount > HashHelpers.HashCollisionThreshold && + _comparer is NonRandomizedStringEqualityComparer) + { + // Switch to a randomized comparer and rehash. + Resize(entries.Length, forceNewHashCodes: true); + } + } + + /// Grow or shrink and to the specified capacity. + [MemberNotNull(nameof(_buckets))] + [MemberNotNull(nameof(_entries))] + private void Resize(int newSize, bool forceNewHashCodes = false) + { + Debug.Assert(!forceNewHashCodes || !typeof(TKey).IsValueType, "Value types never rehash."); + Debug.Assert(newSize >= _count, "The requested size must accomodate all of the current elements."); + + // Create the new arrays. We allocate both prior to storing either; in case one of the allocation fails, + // we want to avoid corrupting the data structure. + int[] newBuckets = new int[newSize]; + Entry[] newEntries = new Entry[newSize]; + if (IntPtr.Size == 8) + { + // Any time the capacity changes, that impacts the divisor of modulo operations, + // and we need to update our fast modulo multiplier. + _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize); + } + + // Copy the existing entries to the new entries array. + int count = _count; + if (_entries is not null) + { + Array.Copy(_entries, newEntries, count); + } + + // If we're being asked to upgrade to a non-randomized comparer due to too many collisions, do so. + if (!typeof(TKey).IsValueType && forceNewHashCodes) + { + // Store the original randomized comparer instead of the non-randomized one. + Debug.Assert(_comparer is NonRandomizedStringEqualityComparer); + IEqualityComparer comparer = _comparer = (IEqualityComparer)((NonRandomizedStringEqualityComparer)_comparer).GetUnderlyingEqualityComparer(); + Debug.Assert(_comparer is not null); + Debug.Assert(_comparer is not NonRandomizedStringEqualityComparer); + + // Update all of the entries' hash codes based on the new comparer. + for (int i = 0; i < count; i++) + { + newEntries[i].HashCode = (uint)comparer.GetHashCode(newEntries[i].Key); + } + } + + // Now publish the buckets array. It's necessary to do this prior to the below loop, + // as PushEntryIntoBucket will be populating _buckets. + _buckets = newBuckets; + + // Populate the buckets. + for (int i = 0; i < count; i++) + { + PushEntryIntoBucket(ref newEntries[i], i); + } + + _entries = newEntries; + } + + /// Gets the bucket assigned to the specified hash code. + /// + /// Buckets are 1-based. This is so that the default initialized value of 0 + /// maps to -1 and is usable as a sentinel. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref int GetBucket(uint hashCode) + { + int[]? buckets = _buckets; + Debug.Assert(buckets is not null); + + if (IntPtr.Size == 8) + { + return ref buckets[HashHelpers.FastMod(hashCode, (uint)buckets.Length, _fastModMultiplier)]; + } + else + { + return ref buckets[(uint)hashCode % buckets.Length]; + } + } + + /// Returns an enumerator that iterates through the . + /// A structure for the . + public Enumerator GetEnumerator() => new(this, useDictionaryEntry: false); + /// IEnumerator> IEnumerable>.GetEnumerator() => Count == 0 ? EnumerableHelpers.GetEmptyEnumerator>() : @@ -537,10 +1118,14 @@ int IList>.IndexOf(KeyValuePair item) { ArgumentNullException.ThrowIfNull(item.Key, nameof(item)); - if (_dictionary.TryGetValue(item.Key, out TValue? value) && - EqualityComparer.Default.Equals(value, item.Value)) + int index = IndexOf(item.Key); + if (index >= 0) { - return _list.IndexOf(item.Key); + Debug.Assert(_entries is not null); + if (EqualityComparer.Default.Equals(item.Value, _entries[index].Value)) + { + return index; + } } return -1; @@ -558,7 +1143,7 @@ bool ICollection>.Contains(KeyValuePair ArgumentNullException.ThrowIfNull(item.Key, nameof(item)); return - _dictionary.TryGetValue(item.Key, out TValue? value) && + TryGetValue(item.Key, out TValue? value) && EqualityComparer.Default.Equals(value, item.Value); } @@ -567,14 +1152,15 @@ void ICollection>.CopyTo(KeyValuePair[] { ArgumentNullException.ThrowIfNull(array); ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); - if (array.Length - arrayIndex < Count) + if (array.Length - arrayIndex < _count) { throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall); } - foreach (TKey key in _list) + for (int i = 0; i < _count; i++) { - array[arrayIndex++] = new(key, _dictionary[key]); + ref Entry entry = ref _entries![i]; + array[arrayIndex++] = new(entry.Key, entry.Value); } } @@ -588,7 +1174,7 @@ bool ICollection>.Remove(KeyValuePair i void IDictionary.Add(object key, object? value) { ArgumentNullException.ThrowIfNull(key); - if (default(TValue) != null) + if (default(TValue) is not null) { ArgumentNullException.ThrowIfNull(value); } @@ -653,7 +1239,7 @@ void ICollection.CopyTo(Array array, int index) ArgumentOutOfRangeException.ThrowIfNegative(index); - if (array.Length - index < _dictionary.Count) + if (array.Length - index < _count) { throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall); } @@ -698,7 +1284,7 @@ int IList.Add(object? value) /// bool IList.Contains(object? value) => value is KeyValuePair pair && - _dictionary.TryGetValue(pair.Key, out TValue? v) && + TryGetValue(pair.Key, out TValue? v) && EqualityComparer.Default.Equals(v, pair.Value); /// @@ -732,32 +1318,37 @@ void IList.Remove(object? value) } } - /// Provides debug validation of the consistency of the collection. - [Conditional("DEBUG")] - private void AssertInvariants() + /// Represents a key/value pair in the dictionary. + private struct Entry { - Debug.Assert(_dictionary.Count == _list.Count, $"Expected dictionary count {_dictionary.Count} to equal list count {_list.Count}"); - foreach (TKey key in _list) - { - Debug.Assert(_dictionary.ContainsKey(key), $"Expected dictionary to contain key {key}"); - } + /// The index of the next entry in the chain, or -1 if this is the last entry in the chain. + public int Next; + /// Cached hash code of . + public uint HashCode; + /// The key. + public TKey Key; + /// The value associated with . + public TValue Value; } /// Enumerates the elements of a . + [StructLayout(LayoutKind.Auto)] public struct Enumerator : IEnumerator>, IDictionaryEnumerator { /// The dictionary being enumerated. private readonly OrderedDictionary _dictionary; - /// The wrapped ordered enumerator. - private List.Enumerator _keyEnumerator; + /// A snapshot of the dictionary's version when enumeration began. + private readonly int _version; /// Whether Current should be a DictionaryEntry. - private bool _useDictionaryEntry; + private readonly bool _useDictionaryEntry; + /// The current index. + private int _index; /// Initialize the enumerator. internal Enumerator(OrderedDictionary dictionary, bool useDictionaryEntry) { _dictionary = dictionary; - _keyEnumerator = dictionary._list.GetEnumerator(); + _version = _dictionary._version; _useDictionaryEntry = useDictionaryEntry; } @@ -781,24 +1372,45 @@ internal Enumerator(OrderedDictionary dictionary, bool useDictiona /// public bool MoveNext() { - if (_keyEnumerator.MoveNext()) + OrderedDictionary dictionary = _dictionary; + + if (_version != dictionary._version) { - Current = new(_keyEnumerator.Current, _dictionary._dictionary[_keyEnumerator.Current]); + ThrowHelper.ThrowVersionCheckFailed(); + } + + if (_index < dictionary._count) + { + Debug.Assert(dictionary._entries is not null); + ref Entry entry = ref dictionary._entries[_index]; + Current = new KeyValuePair(entry.Key, entry.Value); + _index++; return true; } - Current = default!; + Current = default; return false; } /// - void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + void IEnumerator.Reset() + { + if (_version != _dictionary._version) + { + ThrowHelper.ThrowVersionCheckFailed(); + } + + _index = 0; + Current = default; + } /// readonly void IDisposable.Dispose() { } } /// Represents the collection of keys in a . + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] public sealed class KeyCollection : IList, IReadOnlyList, IList { /// The dictionary whose keys are being exposed. @@ -832,28 +1444,90 @@ public sealed class KeyCollection : IList, IReadOnlyList, IList bool IList.Contains(object? value) => value is TKey key && Contains(key); /// - public void CopyTo(TKey[] array, int arrayIndex) => _dictionary._list.CopyTo(array, arrayIndex); + public void CopyTo(TKey[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + + OrderedDictionary dictionary = _dictionary; + int count = dictionary._count; + + if (array.Length - arrayIndex < count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall, nameof(array)); + } + + Entry[]? entries = dictionary._entries; + for (int i = 0; i < count; i++) + { + Debug.Assert(entries is not null); + array[arrayIndex++] = entries[i].Key; + } + } /// - void ICollection.CopyTo(Array array, int index) => - ((ICollection)_dictionary._list).CopyTo(array, index); + void ICollection.CopyTo(Array array, int index) + { + ArgumentNullException.ThrowIfNull(array); + + if (array.Rank != 1) + { + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); + } + + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (array.Length - index < _dictionary.Count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall); + } + + if (array is TKey[] keys) + { + CopyTo(keys, index); + } + else + { + try + { + if (array is not object?[] objects) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + + foreach (TKey key in this) + { + objects[index++] = key; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + } + } /// TKey IList.this[int index] { - get => _dictionary._list[index]; + get => _dictionary.GetAt(index).Key; set => throw new NotSupportedException(); } /// object? IList.this[int index] { - get => _dictionary._list[index]; + get => _dictionary.GetAt(index).Key; set => throw new NotSupportedException(); } /// - TKey IReadOnlyList.this[int index] => _dictionary._list[index]; + TKey IReadOnlyList.this[int index] => _dictionary.GetAt(index).Key; /// Returns an enumerator that iterates through the . /// A for the . @@ -906,39 +1580,23 @@ IEnumerator IEnumerable.GetEnumerator() => /// Enumerates the elements of a . public struct Enumerator : IEnumerator { - /// The dictionary whose keys are being enumerated. - private readonly OrderedDictionary _dictionary; - /// The wrapped ordered enumerator. - private List.Enumerator _keyEnumerator; + /// The dictionary's enumerator. + private OrderedDictionary.Enumerator _enumerator; /// Initialize the enumerator. - internal Enumerator(OrderedDictionary dictionary) - { - _dictionary = dictionary; - _keyEnumerator = dictionary._list.GetEnumerator(); - } + internal Enumerator(OrderedDictionary dictionary) => _enumerator = dictionary.GetEnumerator(); /// - public TKey Current { get; private set; } = default!; + public TKey Current => _enumerator.Current.Key; /// - readonly object IEnumerator.Current => Current; + object IEnumerator.Current => Current; /// - public bool MoveNext() - { - if (_keyEnumerator.MoveNext()) - { - Current = _keyEnumerator.Current; - return true; - } - - Current = default!; - return false; - } + public bool MoveNext() => _enumerator.MoveNext(); /// - void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _enumerator); /// readonly void IDisposable.Dispose() { } @@ -946,6 +1604,8 @@ readonly void IDisposable.Dispose() { } } /// Represents the collection of values in a . + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [DebuggerDisplay("Count = {Count}")] public sealed class ValueCollection : IList, IReadOnlyList, IList { /// The dictionary whose values are being exposed. @@ -977,14 +1637,20 @@ public void CopyTo(TValue[] array, int arrayIndex) { ArgumentNullException.ThrowIfNull(array); ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); - if (array.Length - arrayIndex < Count) + + OrderedDictionary dictionary = _dictionary; + int count = dictionary._count; + + if (array.Length - arrayIndex < count) { throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall, nameof(array)); } - for (int i = 0; i < _dictionary.Count; i++) + Entry[]? entries = dictionary._entries; + for (int i = 0; i < count; i++) { - array[arrayIndex++] = _dictionary._dictionary[_dictionary._list[i]]; + Debug.Assert(entries is not null); + array[arrayIndex++] = entries[i].Value; } } @@ -995,22 +1661,22 @@ public void CopyTo(TValue[] array, int arrayIndex) /// TValue IList.this[int index] { - get => _dictionary[_dictionary._list[index]]; + get => _dictionary.GetAt(index).Value; set => throw new NotSupportedException(); } /// - TValue IReadOnlyList.this[int index] => _dictionary._dictionary[_dictionary._list[index]]; + TValue IReadOnlyList.this[int index] => _dictionary.GetAt(index).Value; /// object? IList.this[int index] { - get => _dictionary[_dictionary._list[index]]; + get => _dictionary.GetAt(index).Value; set => throw new NotSupportedException(); } /// - bool ICollection.Contains(TValue item) => _dictionary._dictionary.ContainsValue(item); + bool ICollection.Contains(TValue item) => _dictionary.ContainsValue(item); /// IEnumerator IEnumerable.GetEnumerator() => @@ -1023,11 +1689,16 @@ IEnumerator IEnumerable.GetEnumerator() => /// int IList.IndexOf(TValue item) { - for (int i = 0; i < _dictionary.Count; i++) + Entry[]? entries = _dictionary._entries; + if (entries is not null) { - if (EqualityComparer.Default.Equals(_dictionary._dictionary[_dictionary._list[i]], item)) + int count = _dictionary._count; + for (int i = 0; i < count; i++) { - return i; + if (EqualityComparer.Default.Equals(item, entries[i].Value)) + { + return i; + } } } @@ -1057,29 +1728,36 @@ int IList.IndexOf(TValue item) /// bool IList.Contains(object? value) => - value is null && default(TValue) is null ? _dictionary.ContainsValue(default!) : - value is TValue tvalue && _dictionary.ContainsValue(tvalue); + value is null && default(TValue) is null ? + _dictionary.ContainsValue(default!) : + value is TValue tvalue && _dictionary.ContainsValue(tvalue); /// int IList.IndexOf(object? value) { - if (value is null && default(TValue) is null) + Entry[]? entries = _dictionary._entries; + if (entries is not null) { - for (int i = 0; i < _dictionary.Count; i++) + int count = _dictionary._count; + + if (value is null && default(TValue) is null) { - if (_dictionary[_dictionary._list[i]] is null) + for (int i = 0; i < count; i++) { - return i; + if (entries[i].Value is null) + { + return i; + } } } - } - else if (value is TValue tvalue) - { - for (int i = 0; i < _dictionary.Count; i++) + else if (value is TValue tvalue) { - if (EqualityComparer.Default.Equals(tvalue, _dictionary[_dictionary._list[i]])) + for (int i = 0; i < count; i++) { - return i; + if (EqualityComparer.Default.Equals(tvalue, entries[i].Value)) + { + return i; + } } } } @@ -1146,43 +1824,41 @@ void ICollection.CopyTo(Array array, int index) /// Enumerates the elements of a . public struct Enumerator : IEnumerator { - /// The dictionary whose keys are being enumerated. - private readonly OrderedDictionary _dictionary; - /// The wrapped ordered enumerator. - private List.Enumerator _keyEnumerator; + /// The dictionary's enumerator. + private OrderedDictionary.Enumerator _enumerator; /// Initialize the enumerator. - internal Enumerator(OrderedDictionary dictionary) - { - _dictionary = dictionary; - _keyEnumerator = dictionary._list.GetEnumerator(); - } + internal Enumerator(OrderedDictionary dictionary) => _enumerator = dictionary.GetEnumerator(); /// - public TValue Current { get; private set; } = default!; + public TValue Current => _enumerator.Current.Value; /// - readonly object? IEnumerator.Current => Current; + object? IEnumerator.Current => Current; /// - public bool MoveNext() - { - if (_keyEnumerator.MoveNext()) - { - Current = _dictionary._dictionary[_keyEnumerator.Current]; - return true; - } - - Current = default!; - return false; - } + public bool MoveNext() => _enumerator.MoveNext(); /// - void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _enumerator); /// readonly void IDisposable.Dispose() { } } } } + + /// Used to control behavior of insertion into a . + /// Not nested in to avoid multiple generic instantiations. + internal enum InsertionBehavior + { + /// Skip the insertion operation. + IgnoreInsertion = 0, + + /// Specifies that an existing entry with the same key should be overwritten if encountered. + OverwriteExisting = 1, + + /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown. + ThrowOnExisting = 2 + } } diff --git a/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs b/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs new file mode 100644 index 0000000000000..f1c9bc60e90fd --- /dev/null +++ b/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections +{ + internal static class ThrowHelper + { + /// Throws an exception for a key not being found in the dictionary. + [DoesNotReturn] + internal static void ThrowKeyNotFound(TKey key) => + throw new KeyNotFoundException(SR.Format(SR.Arg_KeyNotFoundWithKey, key)); + + /// Throws an exception for trying to insert a duplicate key into the dictionary. + [DoesNotReturn] + internal static void ThrowDuplicateKey(TKey key) => + throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key), nameof(key)); + + /// Throws an exception when erroneous concurrent use of a collection is detected. + [DoesNotReturn] + internal static void ThrowConcurrentOperation() => + throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported); + + /// Throws an exception for an index being out of range. + [DoesNotReturn] + internal static void ThrowIndexOutOfRange() => + throw new ArgumentOutOfRangeException("index"); + + /// Throws an exception for a version check failing during enumeration. + [DoesNotReturn] + internal static void ThrowVersionCheckFailed() => + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + } +} diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs index edff8b988c3c6..23ba13609377a 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs @@ -64,7 +64,6 @@ public class OrderedDictionary_Generic_Tests_Keys_AsICollection : ICollection_No protected override ICollection NonGenericICollectionFactory() => new OrderedDictionary().Keys; protected override bool SupportsSerialization => false; protected override Type ICollection_NonGeneric_CopyTo_ArrayOfEnumType_ThrowType => typeof(ArgumentException); - protected override Type ICollection_NonGeneric_CopyTo_NonZeroLowerBound_ThrowType => typeof(ArgumentOutOfRangeException); protected override ICollection NonGenericICollectionFactory(int count) { diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs index 7730716d6b61b..c427c6c4f48ce 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs @@ -36,24 +36,28 @@ public void OrderedDictionary_Generic_Constructor() Assert.Empty(instance.Keys); Assert.Empty(instance.Values); Assert.Same(EqualityComparer.Default, instance.Comparer); + Assert.Equal(0, instance.Capacity); instance = new OrderedDictionary(42); Assert.Empty(instance); Assert.Empty(instance.Keys); Assert.Empty(instance.Values); Assert.Same(EqualityComparer.Default, instance.Comparer); + Assert.InRange(instance.Capacity, 42, int.MaxValue); instance = new OrderedDictionary(comparer); Assert.Empty(instance); Assert.Empty(instance.Keys); Assert.Empty(instance.Values); Assert.Same(comparer, instance.Comparer); + Assert.Equal(0, instance.Capacity); instance = new OrderedDictionary(42, comparer); Assert.Empty(instance); Assert.Empty(instance.Keys); Assert.Empty(instance.Values); Assert.Same(comparer, instance.Comparer); + Assert.InRange(instance.Capacity, 42, int.MaxValue); } [Theory] @@ -87,10 +91,12 @@ public void OrderedDictionary_Generic_Constructor_IEnumerable(int count) copied = new OrderedDictionary(source); Assert.Equal(source, copied); Assert.Same(comparer, EqualityComparer.Default); + Assert.InRange(copied.Capacity, copied.Count, int.MaxValue); copied = new OrderedDictionary(source, comparer); Assert.Equal(source, copied); Assert.Same(comparer, copied.Comparer); + Assert.InRange(copied.Capacity, copied.Count, int.MaxValue); } } @@ -106,6 +112,73 @@ public void OrderedDictionary_Generic_Constructor_NullIDictionary_ThrowsArgument AssertExtensions.Throws("collection", () => new OrderedDictionary((IEnumerable>)null, EqualityComparer.Default)); } + [Fact] + public void OrderedDictionary_Generic_Constructor_AllKeysEqualComparer() + { + var dictionary = new OrderedDictionary(EqualityComparer.Create((x, y) => true, x => 1)); + Assert.Equal(0, dictionary.Count); + + Assert.True(dictionary.TryAdd(CreateTKey(0), CreateTValue(0))); + Assert.Equal(1, dictionary.Count); + + Assert.False(dictionary.TryAdd(CreateTKey(1), CreateTValue(0))); + Assert.Equal(1, dictionary.Count); + + dictionary.Remove(CreateTKey(2)); + Assert.Equal(0, dictionary.Count); + } + + #endregion + + #region TryAdd + [Fact] + public void TryAdd_NullKeyThrows() + { + if (default(TKey) is not null) + { + return; + } + + var dictionary = new OrderedDictionary(); + AssertExtensions.Throws("key", () => dictionary.TryAdd(default(TKey), CreateTValue(0))); + Assert.True(dictionary.TryAdd(CreateTKey(0), default)); + Assert.Equal(1, dictionary.Count); + } + + [Fact] + public void TryAdd_AppendsItemToEndOfDictionary() + { + var dictionary = new OrderedDictionary(); + AddToCollection(dictionary, 10); + foreach (var entry in dictionary) + { + Assert.False(dictionary.TryAdd(entry.Key, entry.Value)); + } + + TKey newKey; + int i = 0; + do + { + newKey = CreateTKey(i); + } + while (dictionary.ContainsKey(newKey)); + + Assert.True(dictionary.TryAdd(newKey, CreateTValue(42))); + Assert.Equal(dictionary.Count - 1, dictionary.IndexOf(newKey)); + } + + [Fact] + public void TryAdd_ItemAlreadyExists_DoesNotInvalidateEnumerator() + { + TKey key1 = CreateTKey(1); + + var dictionary = new OrderedDictionary() { [key1] = CreateTValue(2) }; + + IEnumerator valuesEnum = dictionary.GetEnumerator(); + Assert.False(dictionary.TryAdd(key1, CreateTValue(3))); + + Assert.True(valuesEnum.MoveNext()); + } #endregion #region ContainsValue @@ -198,6 +271,10 @@ public void OrderedDictionary_Generic_SetAt_GetAt_InvalidInputs() dictionary.Add(CreateTKey(1), CreateTValue(1)); TKey firstKey = dictionary.GetAt(0).Key; + dictionary.SetAt(0, firstKey, CreateTValue(0)); + dictionary.SetAt(0, CreateTKey(2), CreateTValue(0)); + dictionary.SetAt(0, firstKey, CreateTValue(0)); + AssertExtensions.Throws("key", () => dictionary.SetAt(1, firstKey, CreateTValue(0))); } @@ -257,6 +334,51 @@ public void OrderedDictionary_Generic_TrimExcess(int count) int dictCount = dictionary.Count; dictionary.TrimExcess(); Assert.Equal(dictCount, dictionary.Count); + Assert.InRange(dictionary.Capacity, dictCount, int.MaxValue); + + if (count > 0) + { + int oldCapacity = dictionary.Capacity; + int newCapacity = dictionary.EnsureCapacity(count * 10); + Assert.Equal(newCapacity, dictionary.Capacity); + Assert.InRange(newCapacity, oldCapacity + 1, int.MaxValue); + dictionary.TrimExcess(dictCount); + Assert.Equal(oldCapacity, dictionary.Capacity); + } + } + + #endregion + + #region EnsureCapacity + + [Fact] + public void OrderedDictionary_Generic_EnsureCapacity() + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(); + + Assert.Equal(0, dictionary.Capacity); + for (int i = 0; i < 10; i++) + { + dictionary.TryAdd(CreateTKey(i), CreateTValue(i)); + } + int count = dictionary.Count; + Assert.InRange(count, 1, 10); + Assert.InRange(dictionary.Capacity, dictionary.Count, int.MaxValue); + Assert.Equal(dictionary.Capacity, dictionary.EnsureCapacity(dictionary.Capacity)); + Assert.Equal(dictionary.Capacity, dictionary.EnsureCapacity(dictionary.Capacity - 1)); + Assert.Equal(dictionary.Capacity, dictionary.EnsureCapacity(0)); + AssertExtensions.Throws(() => dictionary.EnsureCapacity(-1)); + + int oldCapacity = dictionary.Capacity; + int newCapacity = dictionary.EnsureCapacity(oldCapacity * 2); + Assert.Equal(newCapacity, dictionary.Capacity); + Assert.InRange(newCapacity, oldCapacity * 2, int.MaxValue); + + for (int i = 0; i < 10; i++) + { + Assert.True(dictionary.ContainsKey(CreateTKey(i))); + } + Assert.Equal(count, dictionary.Count); } #endregion diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs index eaad2fff191ef..f87548107b0d6 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs @@ -26,6 +26,7 @@ protected override string CreateTKey(int seed) public class OrderedDictionary_Generic_Tests_int_int : OrderedDictionary_Generic_Tests { protected override bool DefaultValueAllowed { get { return true; } } + protected override KeyValuePair CreateT(int seed) { Random rand = new Random(seed); diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs index 0bc4aa6a099fe..e9fd08158be59 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs @@ -108,7 +108,6 @@ public class OrderedDictionary_Keys_IList_NonGeneric_Tests : IList_NonGeneric_Te protected override bool SupportsSerialization => false; protected override Type ICollection_NonGeneric_CopyTo_ArrayOfEnumType_ThrowType => typeof(ArgumentException); protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Enumerable.Empty(); - protected override Type ICollection_NonGeneric_CopyTo_NonZeroLowerBound_ThrowType => typeof(ArgumentOutOfRangeException); protected override bool IsReadOnly => true; protected override object CreateT(int seed) => diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs index bedb53dc57b4d..e5af9af5259c0 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs @@ -178,7 +178,7 @@ public void IDictionary_NonGeneric_Contains_KeyOfWrongType() public void CantAcceptDuplicateKeysFromSourceDictionary() { Dictionary source = new Dictionary { { "a", 1 }, { "A", 1 } }; - AssertExtensions.Throws(null, () => new OrderedDictionary(source, StringComparer.OrdinalIgnoreCase)); + AssertExtensions.Throws("key", () => new OrderedDictionary(source, StringComparer.OrdinalIgnoreCase)); } [Theory]