From a3f12a143235c47606a3147e8483bd6ebc111317 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 9 May 2024 14:27:39 -0400 Subject: [PATCH 1/6] Add OrderedDictionary --- .../Collections/Generic/EnumerableHelpers.cs | 4 + .../IDictionary.NonGeneric.Tests.cs | 3 +- .../IEnumerable.NonGeneric.Tests.cs | 18 +- .../System/Collections/IList.Generic.Tests.cs | 24 +- .../Collections/IList.NonGeneric.Tests.cs | 17 +- .../ref/System.Collections.cs | 165 +++ .../src/System.Collections.csproj | 1 + .../Collections/Generic/OrderedDictionary.cs | 1188 +++++++++++++++++ .../OrderedDictionary.Generic.Tests.Keys.cs | 106 ++ .../OrderedDictionary.Generic.Tests.Values.cs | 113 ++ .../OrderedDictionary.Generic.Tests.cs | 288 ++++ .../OrderedDictionary.Generic.cs | 60 + .../OrderedDictionary.IList.Tests.cs | 225 ++++ .../OrderedDictionary.Tests.cs | 225 ++++ .../tests/System.Collections.Tests.csproj | 6 + 15 files changed, 2434 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs create mode 100644 src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs diff --git a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs index f73a2d3193f32..00b05b11cc759 100644 --- a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs +++ b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs @@ -8,6 +8,10 @@ namespace System.Collections.Generic /// internal static partial class EnumerableHelpers { + /// Calls Reset on an enumerator instance. + /// Enables Reset to be called without boxing on a struct enumerator that lacks a public Reset. + internal static void Reset(ref T enumerator) where T : IEnumerator => enumerator.Reset(); + /// Gets an enumerator singleton for an empty collection. internal static IEnumerator GetEmptyEnumerator() => ((IEnumerable)Array.Empty()).GetEnumerator(); diff --git a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs index adca2780d96d5..3ce765467e512 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs @@ -491,7 +491,6 @@ public virtual void IDictionary_NonGeneric_Values_Enumeration_ParentDictionaryMo { Assert.Throws(() => valuesEnum.MoveNext()); Assert.Throws(() => valuesEnum.Reset()); - Assert.Throws(() => valuesEnum.Current); } else { @@ -832,7 +831,7 @@ public virtual void IDictionary_NonGeneric_IDictionaryEnumerator_Current_AfterEn object current, key, value, entry; IDictionaryEnumerator enumerator = NonGenericIDictionaryFactory(count).GetEnumerator(); while (enumerator.MoveNext()) ; - if (Enumerator_Current_UndefinedOperation_Throws) + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws) { Assert.Throws(() => enumerator.Current); Assert.Throws(() => enumerator.Key); diff --git a/src/libraries/Common/tests/System/Collections/IEnumerable.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/IEnumerable.NonGeneric.Tests.cs index a441cf4d78733..b4877c902d07f 100644 --- a/src/libraries/Common/tests/System/Collections/IEnumerable.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IEnumerable.NonGeneric.Tests.cs @@ -60,6 +60,18 @@ public abstract partial class IEnumerable_NonGeneric_Tests : TestBase /// protected virtual bool Enumerator_Current_UndefinedOperation_Throws => false; + /// + /// When calling Current of the empty enumerator before the first MoveNext, after the end of the collection, + /// or after modification of the enumeration, the resulting behavior is undefined. Tests are included + /// to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// false. + /// + protected virtual bool Enumerator_Empty_Current_UndefinedOperation_Throw => Enumerator_Current_UndefinedOperation_Throws; + /// /// When calling MoveNext or Reset after modification of the enumeration, the resulting behavior is /// undefined. Tests are included to cover two behavioral scenarios: @@ -305,7 +317,7 @@ public virtual void Enumerator_Current_BeforeFirstMoveNext_UndefinedBehavior(int object current; IEnumerable enumerable = NonGenericIEnumerableFactory(count); IEnumerator enumerator = enumerable.GetEnumerator(); - if (Enumerator_Current_UndefinedOperation_Throws) + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws) Assert.Throws(() => enumerator.Current); else current = enumerator.Current; @@ -319,7 +331,7 @@ public virtual void Enumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(in IEnumerable enumerable = NonGenericIEnumerableFactory(count); IEnumerator enumerator = enumerable.GetEnumerator(); while (enumerator.MoveNext()) ; - if (Enumerator_Current_UndefinedOperation_Throws) + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws) Assert.Throws(() => enumerator.Current); else current = enumerator.Current; @@ -336,7 +348,7 @@ public virtual void Enumerator_Current_ModifiedDuringEnumeration_UndefinedBehavi IEnumerator enumerator = enumerable.GetEnumerator(); if (ModifyEnumerable(enumerable)) { - if (Enumerator_Current_UndefinedOperation_Throws) + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws) Assert.Throws(() => enumerator.Current); else current = enumerator.Current; diff --git a/src/libraries/Common/tests/System/Collections/IList.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/IList.Generic.Tests.cs index 44f67e0ca24f8..811ac67950ac0 100644 --- a/src/libraries/Common/tests/System/Collections/IList.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IList.Generic.Tests.cs @@ -103,8 +103,15 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper public void IList_Generic_ItemGet_NegativeIndex_ThrowsException(int count) { IList list = GenericIListFactory(count); + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[-1]); Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[int.MinValue]); + + if (list is IReadOnlyList rol) + { + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[-1]); + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[int.MinValue]); + } } [Theory] @@ -112,8 +119,15 @@ public void IList_Generic_ItemGet_NegativeIndex_ThrowsException(int count) public void IList_Generic_ItemGet_IndexGreaterThanListCount_ThrowsException(int count) { IList list = GenericIListFactory(count); + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count]); Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count + 1]); + + if (list is IReadOnlyList rol) + { + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count]); + Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count + 1]); + } } [Theory] @@ -122,7 +136,15 @@ public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count) { IList list = GenericIListFactory(count); T result; + Assert.All(Enumerable.Range(0, count), index => result = list[index]); + Assert.All(Enumerable.Range(0, count), index => Assert.Equal(list[index], list[index])); + + if (list is IReadOnlyList rol) + { + Assert.All(Enumerable.Range(0, count), index => result = rol[index]); + Assert.All(Enumerable.Range(0, count), index => Assert.Equal(rol[index], rol[index])); + } } #endregion @@ -369,7 +391,7 @@ public void IList_Generic_IndexOf_InvalidValue(int count) [MemberData(nameof(ValidCollectionSizes))] public void IList_Generic_IndexOf_ReturnsFirstMatchingValue(int count) { - if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported) + if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DuplicateValuesAllowed) { IList list = GenericIListFactory(count); foreach (T duplicate in list.ToList()) // hard copies list to circumvent enumeration error diff --git a/src/libraries/Common/tests/System/Collections/IList.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/IList.NonGeneric.Tests.cs index b823b530b3383..2b88b3ef7561f 100644 --- a/src/libraries/Common/tests/System/Collections/IList.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IList.NonGeneric.Tests.cs @@ -83,6 +83,17 @@ protected virtual object CreateT(int seed) /// protected virtual bool IList_CurrentAfterAdd_Throws => Enumerator_Current_UndefinedOperation_Throws; + /// + /// When calling Current of the empty enumerator after the end of the list and list is extended by new items. + /// Tests are included to cover two behavioral scenarios: + /// - Throwing an InvalidOperationException + /// - Returning an undefined value. + /// + /// If this property is set to true, the tests ensure that the exception is thrown. The default value is + /// the same as Enumerator_Current_UndefinedOperation_Throws. + /// + protected virtual bool IList_Empty_CurrentAfterAdd_Throws => Enumerator_Empty_Current_UndefinedOperation_Throw; + #endregion #region ICollection Helper Methods @@ -697,7 +708,7 @@ public void IList_NonGeneric_IndexOf_InvalidValue(int count) [MemberData(nameof(ValidCollectionSizes))] public void IList_NonGeneric_IndexOf_ReturnsFirstMatchingValue(int count) { - if (!IsReadOnly && !ExpectedFixedSize) + if (!IsReadOnly && !ExpectedFixedSize && DuplicateValuesAllowed) { IList list = NonGenericIListFactory(count); @@ -1084,7 +1095,7 @@ public void IList_NonGeneric_CurrentAtEnd_AfterAdd(int count) IEnumerator enumerator = collection.GetEnumerator(); while (enumerator.MoveNext()) ; // Go to end of enumerator - if (Enumerator_Current_UndefinedOperation_Throws) + if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws) { Assert.Throws(() => enumerator.Current); // Enumerator.Current should fail } @@ -1099,7 +1110,7 @@ public void IList_NonGeneric_CurrentAtEnd_AfterAdd(int count) { collection.Add(CreateT(seed++)); - if (IList_CurrentAfterAdd_Throws) + if (count == 0 ? IList_Empty_CurrentAfterAdd_Throws : IList_CurrentAfterAdd_Throws) { Assert.Throws(() => enumerator.Current); // Enumerator.Current should fail } diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 70f4abe1a4bf0..515366d6c983f 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -106,6 +106,171 @@ void System.Runtime.Serialization.IDeserializationCallback.OnDeserialization(obj void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } } + public partial class OrderedDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IList>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.Generic.IReadOnlyList>, System.Collections.ICollection, System.Collections.IDictionary, System.Collections.IEnumerable, System.Collections.IList where TKey : notnull + { + public OrderedDictionary() { } + public OrderedDictionary(System.Collections.Generic.IDictionary dictionary) { } + public OrderedDictionary(System.Collections.Generic.IDictionary dictionary, System.Collections.Generic.IEqualityComparer? comparer) { } + public OrderedDictionary(System.Collections.Generic.IEnumerable> collection) { } + public OrderedDictionary(System.Collections.Generic.IEnumerable> collection, System.Collections.Generic.IEqualityComparer? comparer) { } + public OrderedDictionary(System.Collections.Generic.IEqualityComparer? comparer) { } + public OrderedDictionary(int capacity) { } + public OrderedDictionary(int capacity, System.Collections.Generic.IEqualityComparer? comparer) { } + public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } + public int Count { get { throw null; } } + public TValue this[TKey key] { get { throw null; } set { } } + public System.Collections.Generic.OrderedDictionary.KeyCollection Keys { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + System.Collections.Generic.KeyValuePair System.Collections.Generic.IList>.this[int index] { get { throw null; } set { } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } + System.Collections.Generic.KeyValuePair System.Collections.Generic.IReadOnlyList>.this[int index] { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + bool System.Collections.IDictionary.IsFixedSize { get { throw null; } } + bool System.Collections.IDictionary.IsReadOnly { get { throw null; } } + object? System.Collections.IDictionary.this[object key] { get { throw null; } set { } } + System.Collections.ICollection System.Collections.IDictionary.Keys { get { throw null; } } + System.Collections.ICollection System.Collections.IDictionary.Values { get { throw null; } } + bool System.Collections.IList.IsFixedSize { get { throw null; } } + bool System.Collections.IList.IsReadOnly { get { throw null; } } + object? System.Collections.IList.this[int index] { get { throw null; } set { } } + public System.Collections.Generic.OrderedDictionary.ValueCollection Values { get { throw null; } } + 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 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; } + public void Insert(int index, TKey key, TValue value) { } + public bool Remove(TKey key) { throw null; } + public bool Remove(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } + public void RemoveAt(int index) { } + public void SetAt(int index, TKey key, TValue value) { } + public void SetAt(int index, TValue value) { } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + int System.Collections.Generic.IList>.IndexOf(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.IList>.Insert(int index, System.Collections.Generic.KeyValuePair item) { } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + void System.Collections.IDictionary.Add(object key, object? value) { } + bool System.Collections.IDictionary.Contains(object key) { throw null; } + System.Collections.IDictionaryEnumerator System.Collections.IDictionary.GetEnumerator() { throw null; } + void System.Collections.IDictionary.Remove(object key) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.IList.Add(object? value) { throw null; } + bool System.Collections.IList.Contains(object? value) { throw null; } + int System.Collections.IList.IndexOf(object? value) { throw null; } + void System.Collections.IList.Insert(int index, object? value) { } + void System.Collections.IList.Remove(object? value) { } + public void TrimExcess() { } + 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 + { + private object _dummy; + private int _dummyPrimitive; + public readonly System.Collections.Generic.KeyValuePair Current { get { throw null; } } + System.Collections.DictionaryEntry System.Collections.IDictionaryEnumerator.Entry { get { throw null; } } + object System.Collections.IDictionaryEnumerator.Key { get { throw null; } } + object? System.Collections.IDictionaryEnumerator.Value { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } + public sealed partial class KeyCollection : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList + { + internal KeyCollection() { } + public int Count { get { throw null; } } + bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + TKey System.Collections.Generic.IList.this[int index] { get { throw null; } set { } } + TKey System.Collections.Generic.IReadOnlyList.this[int index] { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + bool System.Collections.IList.IsFixedSize { get { throw null; } } + bool System.Collections.IList.IsReadOnly { get { throw null; } } + object? System.Collections.IList.this[int index] { get { throw null; } set { } } + public bool Contains(TKey key) { throw null; } + public void CopyTo(TKey[] array, int arrayIndex) { } + public System.Collections.Generic.OrderedDictionary.KeyCollection.Enumerator GetEnumerator() { throw null; } + void System.Collections.Generic.ICollection.Add(TKey item) { } + void System.Collections.Generic.ICollection.Clear() { } + bool System.Collections.Generic.ICollection.Remove(TKey item) { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.Generic.IList.IndexOf(TKey item) { throw null; } + void System.Collections.Generic.IList.Insert(int index, TKey item) { } + void System.Collections.Generic.IList.RemoveAt(int index) { } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.IList.Add(object? value) { throw null; } + void System.Collections.IList.Clear() { } + bool System.Collections.IList.Contains(object? value) { throw null; } + int System.Collections.IList.IndexOf(object? value) { throw null; } + void System.Collections.IList.Insert(int index, object? value) { } + void System.Collections.IList.Remove(object? value) { } + void System.Collections.IList.RemoveAt(int index) { } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private TKey _Current_k__BackingField; + private object _dummy; + private int _dummyPrimitive; + public readonly TKey Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } + } + public sealed partial class ValueCollection : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList + { + internal ValueCollection() { } + public int Count { get { throw null; } } + bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + TValue System.Collections.Generic.IList.this[int index] { get { throw null; } set { } } + TValue System.Collections.Generic.IReadOnlyList.this[int index] { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + bool System.Collections.IList.IsFixedSize { get { throw null; } } + bool System.Collections.IList.IsReadOnly { get { throw null; } } + object? System.Collections.IList.this[int index] { get { throw null; } set { } } + public void CopyTo(TValue[] array, int arrayIndex) { } + public System.Collections.Generic.OrderedDictionary.ValueCollection.Enumerator GetEnumerator() { throw null; } + void System.Collections.Generic.ICollection.Add(TValue item) { } + void System.Collections.Generic.ICollection.Clear() { } + bool System.Collections.Generic.ICollection.Contains(TValue item) { throw null; } + bool System.Collections.Generic.ICollection.Remove(TValue item) { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.Generic.IList.IndexOf(TValue item) { throw null; } + void System.Collections.Generic.IList.Insert(int index, TValue item) { } + void System.Collections.Generic.IList.RemoveAt(int index) { } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.IList.Add(object? value) { throw null; } + void System.Collections.IList.Clear() { } + bool System.Collections.IList.Contains(object? value) { throw null; } + int System.Collections.IList.IndexOf(object? value) { throw null; } + void System.Collections.IList.Insert(int index, object? value) { } + void System.Collections.IList.Remove(object? value) { } + void System.Collections.IList.RemoveAt(int index) { } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private TValue _Current_k__BackingField; + private object _dummy; + private int _dummyPrimitive; + public readonly TValue Current { get { throw null; } } + object? System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } + } + } public partial class PriorityQueue { public PriorityQueue() { } diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj index 871c508e84104..63264076c8708 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -19,6 +19,7 @@ + diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs new file mode 100644 index 0000000000000..52b80e3edbacb --- /dev/null +++ b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs @@ -0,0 +1,1188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +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. + [DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))] + [DebuggerDisplay("Count = {Count}")] + public class OrderedDictionary : + IDictionary, IReadOnlyDictionary, IDictionary, + 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; + + /// Lazily-initialized wrapper collection that serves up only the keys, in order. + private KeyCollection? _keys; + /// Lazily-initialized wrapper collection that serves up only the values, in order. + private ValueCollection? _values; + + /// + /// 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() + { + _dictionary = []; + _list = []; + } + + /// + /// Initializes a new instance of the class that is empty, + /// has the specified initial capacity, and uses the default equality comparer for the key type. + /// + /// The initial number of elements that the can contain. + /// capacity is less than 0. + public OrderedDictionary(int capacity) + { + _dictionary = new(capacity); + _list = new(capacity); + } + + /// + /// Initializes a new instance of the class that is empty, + /// has the default initial capacity, and uses the specified . + /// + /// + /// The implementation to use when comparing keys, + /// or null to use the default for the type of the key. + /// + public OrderedDictionary(IEqualityComparer? comparer) + { + _dictionary = new(comparer); + _list = []; + } + + /// + /// Initializes a new instance of the class that is empty, + /// has the specified initial capacity, and uses the specified . + /// + /// The initial number of elements that the can contain. + /// + /// The implementation to use when comparing keys, + /// or null to use the default for the type of the key. + /// + /// capacity is less than 0. + public OrderedDictionary(int capacity, IEqualityComparer? comparer) + { + _dictionary = new(capacity, comparer); + _list = new(capacity); + } + + /// + /// Initializes a new instance of the class that contains elements copied from + /// the specified and uses the default equality comparer for the key type. + /// + /// + /// The whose elements are copied to the new . + /// 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) + { + ArgumentNullException.ThrowIfNull(dictionary); + + int capacity = dictionary.Count; + + _dictionary = new(capacity); + _list = new(capacity); + + AddRange(dictionary); + } + + /// + /// Initializes a new instance of the class that contains elements copied from + /// the specified and uses the specified . + /// + /// + /// The whose elements are copied to the new . + /// The initial order of the elements in the new collection is the order the elements are enumerated from the supplied dictionary. + /// + /// + /// The implementation to use when comparing keys, + /// or null to use the default for the type of the key. + /// + /// is null. + public OrderedDictionary(IDictionary dictionary, IEqualityComparer? comparer) + { + ArgumentNullException.ThrowIfNull(dictionary); + + int capacity = dictionary.Count; + _dictionary = new(capacity, comparer); + _list = new(capacity); + + AddRange(dictionary); + } + + /// + /// Initializes a new instance of the class that contains elements copied + /// from the specified and uses the default equality comparer for the key type. + /// + /// + /// The whose elements are copied to the new . + /// 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) + { + ArgumentNullException.ThrowIfNull(collection); + + int capacity = collection is ICollection> c ? c.Count : 0; + _dictionary = new(capacity); + _list = new(capacity); + + AddRange(collection); + } + + /// + /// Initializes a new instance of the class that contains elements copied + /// from the specified and uses the specified . + /// + /// + /// The whose elements are copied to the new . + /// The initial order of the elements in the new collection is the order the elements are enumerated from the supplied collection. + /// + /// + /// The implementation to use when comparing keys, + /// or null to use the default for the type of the key. + /// + /// is null. + public OrderedDictionary(IEnumerable> collection, IEqualityComparer? comparer) + { + ArgumentNullException.ThrowIfNull(collection); + + int capacity = collection is ICollection> c ? c.Count : 0; + _dictionary = new(capacity, comparer); + _list = new(capacity); + + AddRange(collection); + } + + /// Gets the that is used to determine equality of keys for the dictionary. + public IEqualityComparer Comparer => _dictionary.Comparer; + + /// Gets the number of key/value pairs contained in the . + public int Count => _dictionary.Count; + + /// + bool ICollection>.IsReadOnly => false; + + /// + bool IDictionary.IsReadOnly => false; + + /// + bool IList.IsReadOnly => false; + + /// + bool IDictionary.IsFixedSize => false; + + /// + bool IList.IsFixedSize => false; + + /// Gets a collection containing the keys in the . + public KeyCollection Keys => _keys ??= new(this); + + /// + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + ICollection IDictionary.Keys => Keys; + + /// + ICollection IDictionary.Keys => Keys; + + /// Gets a collection containing the values in the . + public ValueCollection Values => _values ??= new(this); + + /// + IEnumerable IReadOnlyDictionary.Values => Values; + + /// + ICollection IDictionary.Values => Values; + + /// + ICollection IDictionary.Values => Values; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + + /// + object? IList.this[int index] + { + get => GetAt(index); + set + { + ArgumentNullException.ThrowIfNull(value); + + if (value is not KeyValuePair tpair) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, value, typeof(KeyValuePair)), nameof(value)); + } + + SetAt(index, tpair.Key, tpair.Value); + } + } + + /// + object? IDictionary.this[object key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + + if (key is TKey tkey && TryGetValue(tkey, out TValue? value)) + { + return value; + } + + return null; + } + set + { + ArgumentNullException.ThrowIfNull(key); + if (default(TValue) is not null) + { + ArgumentNullException.ThrowIfNull(value); + } + + if (key is not TKey tkey) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, key, typeof(TKey)), nameof(key)); + } + + TValue tvalue = default!; + if (value is not null) + { + if (value is not TValue temp) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, value, typeof(TValue)), nameof(value)); + } + + tvalue = temp; + } + + this[tkey] = tvalue; + } + } + + /// + KeyValuePair IList>.this[int index] + { + get => GetAt(index); + set => SetAt(index, value.Key, value.Value); + } + + /// + KeyValuePair IReadOnlyList>.this[int index] => GetAt(index); + + /// Gets or sets the value associated with the specified key. + /// The key of the value to get or set. + /// The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key. + /// is null. + /// The property is retrieved and does not exist in the collection. + /// Setting the value of an existing key does not impact its order in the collection. + public TValue this[TKey key] + { + get => _dictionary[key]; + set + { + ref TValue? valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, key, out bool keyExists); + + valueRef = value; + if (!keyExists) + { + _list.Add(key); + } + } + } + + /// Adds the specified key and value to the dictionary. + /// 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. + /// An element with the same key already exists in the . + public void Add(TKey key, TValue value) + { + _dictionary.Add(key, value); + _list.Add(key); + } + + /// Adds each element of the enumerable to the dictionary. + private void AddRange(IEnumerable> collection) + { + Debug.Assert(collection is not null); + + if (collection is KeyValuePair[] array) + { + foreach (KeyValuePair pair in array) + { + Add(pair.Key, pair.Value); + } + } + else + { + foreach (KeyValuePair pair in collection) + { + Add(pair.Key, pair.Value); + } + } + } + + /// Removes all keys and values from the . + public void Clear() + { + _dictionary.Clear(); + _list.Clear(); + } + + /// 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); + + /// 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); + + /// Gets the key/value pair at the specified index. + /// The zero-based index of the pair to get. + /// The element at the specified index. + /// is less than 0 or greater than or equal to . + public KeyValuePair GetAt(int index) + { + TKey key = _list[index]; + return new(key, _dictionary[key]); + } + + /// Determines the index of a specific key in the . + /// The key to locate. + /// The index of if found; otherwise, -1. + /// is null. + public int IndexOf(TKey key) + { + ArgumentNullException.ThrowIfNull(key); + + return _list.IndexOf(key); + } + + /// Inserts an item into the collection at the specified index. + /// The zero-based index at which item should be inserted. + /// The key to insert. + /// The value to insert. + /// key is null. + /// An element with the same key already exists in the . + /// is less than 0 or greater than . + public void Insert(int index, TKey key, TValue value) + { + if ((uint)index > (uint)_list.Count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); + } + + _dictionary.Add(key, value); + _list.Insert(index, key); + } + + /// 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; + } + + /// Removes the value with the specified key from the and copies the element to the value parameter. + /// The key of the element to remove. + /// The removed element. + /// 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)) + { + _list.Remove(key); + return true; + } + + return false; + } + + /// Removes the key/value pair at the specified index. + /// The zero-based index of the item to remove. + public void RemoveAt(int index) + { + TKey key = _list[index]; + _list.RemoveAt(index); + _dictionary.Remove(key); + } + + /// 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; + + /// Sets the key/value pair at the specified index. + /// The zero-based index of the element to get or set. + /// The key to store at the specified index. + /// The value to store at the specified index. + /// + public void SetAt(int index, TKey key, TValue value) + { + TKey existing = _list[index]; + + if (_dictionary.ContainsKey(key)) + { + throw new ArgumentException(SR.Format(SR.Argument_AddingDuplicate, key), nameof(key)); + } + + _dictionary.Remove(existing); + _dictionary.Add(key, value); + + _list[index] = key; + } + + /// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries. + public void TrimExcess() + { + _dictionary.TrimExcess(); + _list.TrimExcess(); + } + + /// Gets the value associated with the specified key. + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, if the key is found; + /// 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); + + /// Returns an enumerator that iterates through the . + /// A structure for the . + public Enumerator GetEnumerator() + { + AssertInvariants(); + + return new(this, useDictionaryEntry: false); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() => + Count == 0 ? EnumerableHelpers.GetEmptyEnumerator>() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this, useDictionaryEntry: true); + + /// + 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)) + { + return _list.IndexOf(item.Key); + } + + return -1; + } + + /// + void IList>.Insert(int index, KeyValuePair item) => Insert(index, item.Key, item.Value); + + /// + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); + + /// + bool ICollection>.Contains(KeyValuePair item) + { + ArgumentNullException.ThrowIfNull(item.Key, nameof(item)); + + return + _dictionary.TryGetValue(item.Key, out TValue? value) && + EqualityComparer.Default.Equals(value, item.Value); + } + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall); + } + + foreach (TKey key in _list) + { + array[arrayIndex++] = new(key, _dictionary[key]); + } + } + + /// + bool ICollection>.Remove(KeyValuePair item) => + TryGetValue(item.Key, out TValue? value) && + EqualityComparer.Default.Equals(value, item.Value) && + Remove(item.Key); + + /// + void IDictionary.Add(object key, object? value) + { + ArgumentNullException.ThrowIfNull(key); + if (default(TValue) != null) + { + ArgumentNullException.ThrowIfNull(value); + } + + if (key is not TKey tkey) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, key, typeof(TKey)), nameof(key)); + } + + if (default(TValue) is not null) + { + ArgumentNullException.ThrowIfNull(value); + } + + TValue tvalue = default!; + if (value is not null) + { + if (value is not TValue temp) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, value, typeof(TValue)), nameof(value)); + } + + tvalue = temp; + } + + Add(tkey, tvalue); + } + + /// + bool IDictionary.Contains(object key) + { + ArgumentNullException.ThrowIfNull(key); + + return key is TKey tkey && ContainsKey(tkey); + } + + /// + void IDictionary.Remove(object key) + { + ArgumentNullException.ThrowIfNull(key); + + if (key is TKey tkey) + { + Remove(tkey); + } + } + + /// + 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 KeyValuePair[] tarray) + { + ((ICollection>)this).CopyTo(tarray, index); + } + else + { + try + { + if (array is not object[] objects) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + + foreach (KeyValuePair pair in this) + { + objects[index++] = pair; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + } + } + + /// + int IList.Add(object? value) + { + if (value is not KeyValuePair pair) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, value, typeof(KeyValuePair)), nameof(value)); + } + + Add(pair.Key, pair.Value); + return Count - 1; + } + + /// + bool IList.Contains(object? value) => + value is KeyValuePair pair && + _dictionary.TryGetValue(pair.Key, out TValue? v) && + EqualityComparer.Default.Equals(v, pair.Value); + + /// + int IList.IndexOf(object? value) + { + if (value is KeyValuePair pair) + { + return ((IList>)this).IndexOf(pair); + } + + return -1; + } + + /// + void IList.Insert(int index, object? value) + { + if (value is not KeyValuePair pair) + { + throw new ArgumentException(SR.Format(SR.Arg_WrongType, value, typeof(KeyValuePair)), nameof(value)); + } + + Insert(index, pair.Key, pair.Value); + } + + /// + void IList.Remove(object? value) + { + if (value is KeyValuePair pair) + { + ((ICollection>)this).Remove(pair); + } + } + + /// Provides debug validation of the consistency of the collection. + [Conditional("DEBUG")] + private void AssertInvariants() + { + 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}"); + } + } + + /// Enumerates the elements of a . + public struct Enumerator : IEnumerator>, IDictionaryEnumerator + { + /// The dictionary being enumerated. + private readonly OrderedDictionary _dictionary; + /// The wrapped ordered enumerator. + private List.Enumerator _keyEnumerator; + /// Whether Current should be a DictionaryEntry. + private bool _useDictionaryEntry; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary, bool useDictionaryEntry) + { + _dictionary = dictionary; + _keyEnumerator = dictionary._list.GetEnumerator(); + _useDictionaryEntry = useDictionaryEntry; + } + + /// + public KeyValuePair Current { get; private set; } + + /// + readonly object IEnumerator.Current => _useDictionaryEntry ? + new DictionaryEntry(Current.Key, Current.Value) : + Current; + + /// + readonly DictionaryEntry IDictionaryEnumerator.Entry => new(Current.Key, Current.Value); + + /// + readonly object IDictionaryEnumerator.Key => Current.Key; + + /// + readonly object? IDictionaryEnumerator.Value => Current.Value; + + /// + public bool MoveNext() + { + if (_keyEnumerator.MoveNext()) + { + Current = new(_keyEnumerator.Current, _dictionary._dictionary[_keyEnumerator.Current]); + return true; + } + + Current = default!; + return false; + } + + /// + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + + /// + readonly void IDisposable.Dispose() { } + } + + /// Represents the collection of keys in a . + public sealed class KeyCollection : IList, IReadOnlyList, IList + { + /// The dictionary whose keys are being exposed. + private readonly OrderedDictionary _dictionary; + + /// Initialize the collection wrapper. + internal KeyCollection(OrderedDictionary dictionary) => _dictionary = dictionary; + + /// + public int Count => _dictionary.Count; + + /// + bool ICollection.IsReadOnly => true; + + /// + bool IList.IsReadOnly => true; + + /// + bool IList.IsFixedSize => false; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + + /// + public bool Contains(TKey key) => _dictionary.ContainsKey(key); + + /// + bool IList.Contains(object? value) => value is TKey key && Contains(key); + + /// + public void CopyTo(TKey[] array, int arrayIndex) => _dictionary._list.CopyTo(array, arrayIndex); + + /// + void ICollection.CopyTo(Array array, int index) => + ((ICollection)_dictionary._list).CopyTo(array, index); + + /// + TKey IList.this[int index] + { + get => _dictionary._list[index]; + set => throw new NotSupportedException(); + } + + /// + object? IList.this[int index] + { + get => _dictionary._list[index]; + set => throw new NotSupportedException(); + } + + /// + TKey IReadOnlyList.this[int index] => _dictionary._list[index]; + + /// Returns an enumerator that iterates through the . + /// A for the . + public Enumerator GetEnumerator() => new(_dictionary); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? EnumerableHelpers.GetEmptyEnumerator() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + int IList.IndexOf(TKey item) => _dictionary.IndexOf(item); + + /// + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + /// + void ICollection.Clear() => throw new NotSupportedException(); + + /// + void IList.Insert(int index, TKey item) => throw new NotSupportedException(); + + /// + bool ICollection.Remove(TKey item) => throw new NotSupportedException(); + + /// + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// + int IList.Add(object? value) => throw new NotSupportedException(); + + /// + void IList.Clear() => throw new NotSupportedException(); + + /// + int IList.IndexOf(object? value) => value is TKey key ? _dictionary.IndexOf(key) : -1; + + /// + void IList.Insert(int index, object? value) => throw new NotSupportedException(); + + /// + void IList.Remove(object? value) => throw new NotSupportedException(); + + /// + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// 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; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary) + { + _dictionary = dictionary; + _keyEnumerator = dictionary._list.GetEnumerator(); + } + + /// + public TKey Current { get; private set; } = default!; + + /// + readonly object IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if (_keyEnumerator.MoveNext()) + { + Current = _keyEnumerator.Current; + return true; + } + + Current = default!; + return false; + } + + /// + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + + /// + readonly void IDisposable.Dispose() { } + } + } + + /// Represents the collection of values in a . + public sealed class ValueCollection : IList, IReadOnlyList, IList + { + /// The dictionary whose values are being exposed. + private readonly OrderedDictionary _dictionary; + + /// Initialize the collection wrapper. + internal ValueCollection(OrderedDictionary dictionary) => _dictionary = dictionary; + + /// + public int Count => _dictionary.Count; + + /// + bool ICollection.IsReadOnly => true; + + /// + bool IList.IsReadOnly => true; + + /// + bool IList.IsFixedSize => false; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; + + /// + public void CopyTo(TValue[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall, nameof(array)); + } + + for (int i = 0; i < _dictionary.Count; i++) + { + array[arrayIndex++] = _dictionary._dictionary[_dictionary._list[i]]; + } + } + + /// Returns an enumerator that iterates through the . + /// A for the . + public Enumerator GetEnumerator() => new(_dictionary); + + /// + TValue IList.this[int index] + { + get => _dictionary[_dictionary._list[index]]; + set => throw new NotSupportedException(); + } + + /// + TValue IReadOnlyList.this[int index] => _dictionary._dictionary[_dictionary._list[index]]; + + /// + object? IList.this[int index] + { + get => _dictionary[_dictionary._list[index]]; + set => throw new NotSupportedException(); + } + + /// + bool ICollection.Contains(TValue item) => _dictionary._dictionary.ContainsValue(item); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? EnumerableHelpers.GetEmptyEnumerator() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + int IList.IndexOf(TValue item) + { + for (int i = 0; i < _dictionary.Count; i++) + { + if (EqualityComparer.Default.Equals(_dictionary._dictionary[_dictionary._list[i]], item)) + { + return i; + } + } + + return -1; + } + + /// + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + /// + void ICollection.Clear() => throw new NotSupportedException(); + + /// + void IList.Insert(int index, TValue item) => throw new NotSupportedException(); + + /// + bool ICollection.Remove(TValue item) => throw new NotSupportedException(); + + /// + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// + int IList.Add(object? value) => throw new NotSupportedException(); + + /// + void IList.Clear() => throw new NotSupportedException(); + + /// + bool IList.Contains(object? value) => + 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) + { + for (int i = 0; i < _dictionary.Count; i++) + { + if (_dictionary[_dictionary._list[i]] is null) + { + return i; + } + } + } + else if (value is TValue tvalue) + { + for (int i = 0; i < _dictionary.Count; i++) + { + if (EqualityComparer.Default.Equals(tvalue, _dictionary[_dictionary._list[i]])) + { + return i; + } + } + } + + return -1; + } + + /// + void IList.Insert(int index, object? value) => throw new NotSupportedException(); + + /// + void IList.Remove(object? value) => throw new NotSupportedException(); + + /// + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// + 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 TValue[] values) + { + CopyTo(values, index); + } + else + { + try + { + if (array is not object?[] objects) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + + foreach (TValue value in this) + { + objects[index++] = value; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_IncompatibleArrayType, nameof(array)); + } + } + } + + /// 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; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary) + { + _dictionary = dictionary; + _keyEnumerator = dictionary._list.GetEnumerator(); + } + + /// + public TValue Current { get; private set; } = default!; + + /// + readonly object? IEnumerator.Current => Current; + + /// + public bool MoveNext() + { + if (_keyEnumerator.MoveNext()) + { + Current = _dictionary._dictionary[_keyEnumerator.Current]; + return true; + } + + Current = default!; + return false; + } + + /// + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _keyEnumerator); + + /// + readonly void IDisposable.Dispose() { } + } + } + } +} 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 new file mode 100644 index 0000000000000..edff8b988c3c6 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs @@ -0,0 +1,106 @@ +// 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; +using Xunit; + +namespace System.Collections.Tests +{ + public class OrderedDictionary_Generic_Tests_Keys : ICollection_Generic_Tests + { + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool DefaultValueAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool IsReadOnly => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations)=> new List(); + protected override ICollection GenericICollectionFactory() => new OrderedDictionary().Keys; + + protected override ICollection GenericICollectionFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + int seed = 13453; + for (int i = 0; i < count; i++) + { + dictionary.Add(CreateT(seed++), CreateT(seed++)); + } + + return dictionary.Keys; + } + + protected override string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_KeyCollection_GetEnumerator(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + int seed = 13453; + while (dictionary.Count < count) + { + dictionary.Add(CreateT(seed++), CreateT(seed++)); + } + + dictionary.Keys.GetEnumerator(); + } + } + + public class OrderedDictionary_Generic_Tests_Keys_AsICollection : ICollection_NonGeneric_Tests + { + protected override bool NullAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool IsReadOnly => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + 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) + { + OrderedDictionary dictionary = new OrderedDictionary(); + int seed = 13453; + for (int i = 0; i < count; i++) + { + dictionary.Add(CreateT(seed++), CreateT(seed++)); + } + + return dictionary.Keys; + } + + private string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + protected override void AddToCollection(ICollection collection, int numberOfItemsToAdd) => Debug.Fail("Read only"); + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_KeyCollection_CopyTo_ExactlyEnoughSpaceInTypeCorrectArray(int count) + { + ICollection collection = NonGenericICollectionFactory(count); + string[] array = new string[count]; + collection.CopyTo(array, 0); + int i = 0; + foreach (object obj in collection) + { + Assert.Equal(array[i++], obj); + } + } + } +} diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs new file mode 100644 index 0000000000000..d853f5b24e06c --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs @@ -0,0 +1,113 @@ +// 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; +using Xunit; + +namespace System.Collections.Tests +{ + public class OrderedDictionary_Generic_Tests_Values : ICollection_Generic_Tests + { + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool DefaultValueAllowed => true; + protected override bool DuplicateValuesAllowed => true; + protected override bool IsReadOnly => true; + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override ICollection GenericICollectionFactory() => new OrderedDictionary().Values; + + protected override ICollection GenericICollectionFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 12453; + for (int i = 0; i < count; i++) + { + dictionary.Add(CreateT(seed++), CreateT(seed++)); + } + + return dictionary.Values; + } + + protected override string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ValueCollection_GetEnumerator(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 12453; + while (dictionary.Count < count) + { + dictionary.Add(CreateT(seed++), CreateT(seed++)); + } + + dictionary.Values.GetEnumerator(); + } + } + + public class OrderedDictionary_Generic_Tests_Values_AsICollection : ICollection_NonGeneric_Tests + { + protected override bool NullAllowed => true; + protected override bool DuplicateValuesAllowed => true; + protected override bool IsReadOnly => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + protected override bool SupportsSerialization => false; + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfEnumType_ThrowType => typeof(ArgumentException); + + protected override ICollection NonGenericICollectionFactory() => new OrderedDictionary().Values; + + protected override ICollection NonGenericICollectionFactory(int count) + { + OrderedDictionary list = new OrderedDictionary(); + int seed = 13453; + for (int i = 0; i < count; i++) + { + list.Add(CreateT(seed++), CreateT(seed++)); + } + + return list.Values; + } + + private string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + protected override void AddToCollection(ICollection collection, int numberOfItemsToAdd) + { + Debug.Assert(false); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ValueCollection_CopyTo_ExactlyEnoughSpaceInTypeCorrectArray(int count) + { + ICollection collection = NonGenericICollectionFactory(count); + string[] array = new string[count]; + collection.CopyTo(array, 0); + int i = 0; + foreach (object obj in collection) + { + Assert.Equal(array[i++], obj); + } + } + } +} 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 new file mode 100644 index 0000000000000..7730716d6b61b --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs @@ -0,0 +1,288 @@ +// 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.Linq; +using Xunit; + +namespace System.Collections.Tests +{ + /// + /// Contains tests that ensure the correctness of the Dictionary class. + /// + public abstract class OrderedDictionary_Generic_Tests : IDictionary_Generic_Tests + { + #region IDictionary Helper Methods + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool DefaultValueWhenNotAllowed_Throws => true; + protected override ModifyOperation ModifyEnumeratorThrows => ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Remove | ModifyOperation.Clear; + + protected override IDictionary GenericIDictionaryFactory() => new OrderedDictionary(); + + #endregion + + #region Constructors + + [Fact] + public void OrderedDictionary_Generic_Constructor() + { + OrderedDictionary instance; + IEqualityComparer comparer = GetKeyIEqualityComparer(); + + instance = new OrderedDictionary(); + Assert.Empty(instance); + Assert.Empty(instance.Keys); + Assert.Empty(instance.Values); + Assert.Same(EqualityComparer.Default, instance.Comparer); + + instance = new OrderedDictionary(42); + Assert.Empty(instance); + Assert.Empty(instance.Keys); + Assert.Empty(instance.Values); + Assert.Same(EqualityComparer.Default, instance.Comparer); + + instance = new OrderedDictionary(comparer); + Assert.Empty(instance); + Assert.Empty(instance.Keys); + Assert.Empty(instance.Values); + Assert.Same(comparer, instance.Comparer); + + instance = new OrderedDictionary(42, comparer); + Assert.Empty(instance); + Assert.Empty(instance.Keys); + Assert.Empty(instance.Values); + Assert.Same(comparer, instance.Comparer); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_Constructor_IDictionary(int count) + { + IDictionary source = GenericIDictionaryFactory(count); + IEqualityComparer comparer = GetKeyIEqualityComparer(); + OrderedDictionary copied; + + copied = new OrderedDictionary(source); + Assert.Equal(source, copied); + Assert.Same(comparer, EqualityComparer.Default); + + copied = new OrderedDictionary(source, comparer); + Assert.Equal(source, copied); + Assert.Same(comparer, copied.Comparer); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_Constructor_IEnumerable(int count) + { + IEnumerable> initial = GenericIDictionaryFactory(count); + + foreach (IEnumerable> source in new[] { initial, initial.ToArray(), initial.Where(i => true) }) + { + IEqualityComparer comparer = GetKeyIEqualityComparer(); + OrderedDictionary copied; + + copied = new OrderedDictionary(source); + Assert.Equal(source, copied); + Assert.Same(comparer, EqualityComparer.Default); + + copied = new OrderedDictionary(source, comparer); + Assert.Equal(source, copied); + Assert.Same(comparer, copied.Comparer); + } + } + + [Fact] + public void OrderedDictionary_Generic_Constructor_NullIDictionary_ThrowsArgumentNullException() + { + AssertExtensions.Throws("dictionary", () => new OrderedDictionary((IDictionary)null)); + AssertExtensions.Throws("dictionary", () => new OrderedDictionary((IDictionary)null, null)); + AssertExtensions.Throws("dictionary", () => new OrderedDictionary((IDictionary)null, EqualityComparer.Default)); + + AssertExtensions.Throws("collection", () => new OrderedDictionary((IEnumerable>)null)); + AssertExtensions.Throws("collection", () => new OrderedDictionary((IEnumerable>)null, null)); + AssertExtensions.Throws("collection", () => new OrderedDictionary((IEnumerable>)null, EqualityComparer.Default)); + } + + #endregion + + #region ContainsValue + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ContainsValue_NotPresent(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + int seed = 4315; + TValue notPresent = CreateTValue(seed++); + while (dictionary.Values.Contains(notPresent)) + { + notPresent = CreateTValue(seed++); + } + + Assert.False(dictionary.ContainsValue(notPresent)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ContainsValue_Present(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + int seed = 4315; + KeyValuePair notPresent = CreateT(seed++); + while (dictionary.Contains(notPresent)) + { + notPresent = CreateT(seed++); + } + + dictionary.Add(notPresent.Key, notPresent.Value); + Assert.True(dictionary.ContainsValue(notPresent.Value)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ContainsValue_DefaultValueNotPresent(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + Assert.False(dictionary.ContainsValue(default(TValue))); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_ContainsValue_DefaultValuePresent(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + int seed = 4315; + TKey notPresent = CreateTKey(seed++); + while (dictionary.ContainsKey(notPresent)) + { + notPresent = CreateTKey(seed++); + } + + dictionary.Add(notPresent, default(TValue)); + Assert.True(dictionary.ContainsValue(default(TValue))); + } + + #endregion + + #region GetAt / SetAt + + [Fact] + public void OrderedDictionary_Generic_SetAt_GetAt_InvalidInputs() + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(); + + AssertExtensions.Throws("index", () => dictionary.GetAt(-1)); + AssertExtensions.Throws("index", () => dictionary.GetAt(0)); + AssertExtensions.Throws("index", () => dictionary.SetAt(-1, CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(0, CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(-1, CreateTKey(0), CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(0, CreateTKey(0), CreateTValue(0))); + + dictionary.Add(CreateTKey(0), CreateTValue(0)); + + AssertExtensions.Throws("index", () => dictionary.GetAt(-1)); + AssertExtensions.Throws("index", () => dictionary.GetAt(1)); + AssertExtensions.Throws("index", () => dictionary.SetAt(-1, CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(1, CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(-1, CreateTKey(0), CreateTValue(0))); + AssertExtensions.Throws("index", () => dictionary.SetAt(1, CreateTKey(0), CreateTValue(0))); + + if (default(TKey) is null) + { + AssertExtensions.Throws("key", () => dictionary.SetAt(0, default, CreateTValue(0))); + } + + dictionary.Add(CreateTKey(1), CreateTValue(1)); + + TKey firstKey = dictionary.GetAt(0).Key; + AssertExtensions.Throws("key", () => dictionary.SetAt(1, firstKey, CreateTValue(0))); + } + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void OrderedDictionary_Generic_SetAt_GetAt_Roundtrip(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + KeyValuePair pair; + + for (int i = 0; i < dictionary.Count; i++) + { + pair = dictionary.GetAt(i); + Assert.Equal(pair, ((IList>)dictionary)[i]); + + dictionary.SetAt(i, CreateTValue(i + 500)); + pair = dictionary.GetAt(i); + Assert.Equal(pair, ((IList>)dictionary)[i]); + + dictionary.SetAt(i, CreateTKey(i + 1000), CreateTValue(i + 1000)); + pair = dictionary.GetAt(i); + Assert.Equal(pair, ((IList>)dictionary)[i]); + } + } + + #endregion + + #region Remove(..., out TValue) + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void OrderedDictionary_Generic_Remove(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + + KeyValuePair pair = default; + while (dictionary.Count > 0) + { + pair = dictionary.GetAt(0); + Assert.True(dictionary.Remove(pair.Key, out TValue value)); + Assert.Equal(pair.Value, value); + } + + Assert.False(dictionary.Remove(pair.Key, out _)); + } + + #endregion + + #region TrimExcess + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void OrderedDictionary_Generic_TrimExcess(int count) + { + OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(count); + + int dictCount = dictionary.Count; + dictionary.TrimExcess(); + Assert.Equal(dictCount, dictionary.Count); + } + + #endregion + + #region IReadOnlyDictionary.Keys + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Keys_ContainsAllCorrectKeys(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Key); + IEnumerable keys = ((IReadOnlyDictionary)dictionary).Keys; + Assert.True(expected.SequenceEqual(keys)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Values_ContainsAllCorrectValues(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Value); + IEnumerable values = ((IReadOnlyDictionary)dictionary).Values; + Assert.True(expected.SequenceEqual(values)); + } + + #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 new file mode 100644 index 0000000000000..eaad2fff191ef --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs @@ -0,0 +1,60 @@ +// 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 Xunit; + +namespace System.Collections.Tests +{ + public class OrderedDictionary_Generic_Tests_string_string : OrderedDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) => + new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + + protected override string CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) => CreateTKey(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); + return new KeyValuePair(rand.Next(), rand.Next()); + } + + protected override int CreateTKey(int seed) => new Random(seed).Next(); + + protected override int CreateTValue(int seed) => CreateTKey(seed); + } + + [OuterLoop] + public class OrderedDictionary_Generic_Tests_EquatableBackwardsOrder_int : OrderedDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) + { + Random rand = new Random(seed); + return new KeyValuePair(new EquatableBackwardsOrder(rand.Next()), rand.Next()); + } + + protected override EquatableBackwardsOrder CreateTKey(int seed) + { + Random rand = new Random(seed); + return new EquatableBackwardsOrder(rand.Next()); + } + + protected override int CreateTValue(int seed) => new Random(seed).Next(); + + protected override IDictionary GenericIDictionaryFactory() => + new OrderedDictionary(); + } +} 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 new file mode 100644 index 0000000000000..0bc4aa6a099fe --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs @@ -0,0 +1,225 @@ +// 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.Linq; + +namespace System.Collections.Tests +{ + public class OrderedDictionary_IList_Tests : IList_Generic_Tests> + { + protected override bool DefaultValueAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool DefaultValueWhenNotAllowed_Throws => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + + protected override KeyValuePair CreateT(int seed) => + new KeyValuePair(CreateString(seed), CreateString(seed + 500)); + + protected override IList> GenericIListFactory() => new OrderedDictionary(); + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class OrderedDictionary_IList_NonGeneric_Tests : IList_NonGeneric_Tests + { + protected override bool NullAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override bool SupportsSerialization => false; + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfEnumType_ThrowType => typeof(ArgumentException); + protected override bool IList_Empty_CurrentAfterAdd_Throws => true; + protected override ModifyOperation ModifyEnumeratorThrows => ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Remove | ModifyOperation.Clear; + + protected override object CreateT(int seed) => + new KeyValuePair(CreateString(seed), CreateString(seed + 500)); + + protected override IList NonGenericIListFactory() => new OrderedDictionary(); + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class OrderedDictionary_Keys_IList_Generic_Tests : IList_Generic_Tests + { + protected override bool DefaultValueAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool DefaultValueWhenNotAllowed_Throws => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Enumerable.Empty(); + protected override bool IsReadOnly => true; + + protected override string CreateT(int seed) => CreateString(seed); + + protected override IList GenericIListFactory() => new OrderedDictionary().Keys; + + protected override IList GenericIListFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 42; + while (dictionary.Count < count) + { + string key = CreateT(seed++); + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, CreateT(seed++)); + } + } + + return dictionary.Keys; + } + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class OrderedDictionary_Keys_IList_NonGeneric_Tests : IList_NonGeneric_Tests + { + protected override bool NullAllowed => false; + protected override bool DuplicateValuesAllowed => false; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + 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) => + CreateString(seed); + + protected override IList NonGenericIListFactory() => new OrderedDictionary().Keys; + + protected override IList NonGenericIListFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 42; + while (dictionary.Count < count) + { + string key = CreateString(seed++); + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, CreateString(seed++)); + } + } + + return dictionary.Keys; + } + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class OrderedDictionary_Values_IList_Generic_Tests : IList_Generic_Tests + { + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throws => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Enumerable.Empty(); + protected override bool IsReadOnly => true; + + protected override string CreateT(int seed) => CreateString(seed); + + protected override IList GenericIListFactory() => new OrderedDictionary().Values; + + protected override IList GenericIListFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 42; + while (dictionary.Count < count) + { + string key = CreateT(seed++); + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, CreateT(seed++)); + } + } + + return dictionary.Values; + } + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class OrderedDictionary_Values_IList_NonGeneric_Tests : IList_NonGeneric_Tests + { + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + 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 bool IsReadOnly => true; + + protected override object CreateT(int seed) => + CreateString(seed); + + protected override IList NonGenericIListFactory() => new OrderedDictionary().Values; + + protected override IList NonGenericIListFactory(int count) + { + OrderedDictionary dictionary = new OrderedDictionary(); + + int seed = 42; + while (dictionary.Count < count) + { + string key = CreateString(seed++); + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, CreateString(seed++)); + } + } + + return dictionary.Values; + } + + private string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } +} diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs new file mode 100644 index 0000000000000..bedb53dc57b4d --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs @@ -0,0 +1,225 @@ +// 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 Xunit; + +namespace System.Collections.Tests +{ + public class OrderedDictionary_IDictionary_NonGeneric_Tests : IDictionary_NonGeneric_Tests + { + #region IDictionary Helper Methods + protected override bool Enumerator_Current_UndefinedOperation_Throws => false; + protected override bool Enumerator_Empty_Current_UndefinedOperation_Throw => true; + protected override bool Enumerator_Empty_UsesSingletonInstance => true; + protected override bool Enumerator_Empty_ModifiedDuringEnumeration_ThrowsInvalidOperationException => false; + protected override ModifyOperation ModifyEnumeratorThrows => ModifyOperation.Add | ModifyOperation.Insert | ModifyOperation.Remove | ModifyOperation.Clear; + protected override bool SupportsSerialization => false; + + protected override IDictionary NonGenericIDictionaryFactory() + { + return new OrderedDictionary(); + } + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTValue(int seed) => CreateTKey(seed); + + #endregion + + #region Ordering tests + + [Fact] + public void Ordering_AddInsertRemoveClear_ExpectedOrderResults() + { + OrderedDictionary d = []; + + d.Add(1, 1); + d.Add(2, 2); + d.Add(3, 3); + Assert.Equal(new[] { 1, 2, 3 }, d.Keys); + Assert.Equal(new[] { 1, 2, 3 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(1, 1), KeyValuePair.Create(2, 2), KeyValuePair.Create(3, 3) }, d); + + d.Remove(2); + Assert.Equal(new[] { 1, 3 }, d.Keys); + Assert.Equal(new[] { 1, 3 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(1, 1), KeyValuePair.Create(3, 3) }, d); + + d.Add(4, 4); + Assert.Equal(new[] { 1, 3, 4 }, d.Keys); + Assert.Equal(new[] { 1, 3, 4 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(1, 1), KeyValuePair.Create(3, 3), KeyValuePair.Create(4, 4) }, d); + + d.Insert(0, 5, 5); + Assert.Equal(new[] { 5, 1, 3, 4 }, d.Keys); + Assert.Equal(new[] { 5, 1, 3, 4 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(5, 5), KeyValuePair.Create(1, 1), KeyValuePair.Create(3, 3), KeyValuePair.Create(4, 4) }, d); + + d.RemoveAt(2); + Assert.Equal(new[] { 5, 1, 4 }, d.Keys); + Assert.Equal(new[] { 5, 1, 4 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(5, 5), KeyValuePair.Create(1, 1), KeyValuePair.Create(4, 4) }, d); + + d.Add(6, 6); + Assert.Equal(new[] { 5, 1, 4, 6 }, d.Keys); + Assert.Equal(new[] { 5, 1, 4, 6 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(5, 5), KeyValuePair.Create(1, 1), KeyValuePair.Create(4, 4), KeyValuePair.Create(6, 6) }, d); + + d.Clear(); + Assert.Empty(d.Keys); + Assert.Empty(d.Values); + Assert.Empty(d); + + d.Add(7, 7); + d.Add(9, 9); + d.Add(8, 8); + Assert.Equal(new[] { 7, 9, 8 }, d.Keys); + Assert.Equal(new[] { 7, 9, 8 }, d.Values); + Assert.Equal(new[] { KeyValuePair.Create(7, 7), KeyValuePair.Create(9, 9), KeyValuePair.Create(8, 8) }, d); + } + + #endregion + + #region IDictionary tests + + [Fact] + public void IDictionary_NonGeneric_ItemSet_NullValueWhenDefaultValueIsNonNull() + { + IDictionary dictionary = new OrderedDictionary(); + Assert.Throws(() => dictionary[GetNewKey(dictionary)] = null); + } + + [Fact] + public void IDictionary_NonGeneric_ItemSet_KeyOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + AssertExtensions.Throws("key", () => dictionary[23] = CreateTValue(12345)); + Assert.Empty(dictionary); + } + } + + [Fact] + public void IDictionary_NonGeneric_ItemSet_ValueOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + object missingKey = GetNewKey(dictionary); + AssertExtensions.Throws("value", () => dictionary[missingKey] = 324); + Assert.Empty(dictionary); + } + } + + [Fact] + public void IDictionary_NonGeneric_Add_KeyOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + object missingKey = 23; + AssertExtensions.Throws("key", () => dictionary.Add(missingKey, CreateTValue(12345))); + Assert.Empty(dictionary); + } + } + + [Fact] + public void IDictionary_NonGeneric_Add_ValueOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + object missingKey = GetNewKey(dictionary); + AssertExtensions.Throws("value", () => dictionary.Add(missingKey, 324)); + Assert.Empty(dictionary); + } + } + + [Fact] + public void IDictionary_NonGeneric_Add_NullValueWhenDefaultTValueIsNonNull() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + object missingKey = GetNewKey(dictionary); + Assert.Throws(() => dictionary.Add(missingKey, null)); + Assert.Empty(dictionary); + } + } + + [Fact] + public void IDictionary_NonGeneric_Contains_KeyOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + Assert.False(dictionary.Contains(1)); + } + } + + [Fact] + public void CantAcceptDuplicateKeysFromSourceDictionary() + { + Dictionary source = new Dictionary { { "a", 1 }, { "A", 1 } }; + AssertExtensions.Throws(null, () => new OrderedDictionary(source, StringComparer.OrdinalIgnoreCase)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public override void IDictionary_NonGeneric_IDictionaryEnumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(int count) + { + if (count != 0) + { + // Different undefined behavior for IDictionary.GetEnumerator when empty than for ICollection.GetEnumerator, + // as the former doesn't use a singleton. + base.IDictionary_NonGeneric_IDictionaryEnumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(count); + } + } + + #endregion + + #region ICollection tests + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ICollection_NonGeneric_CopyTo_ArrayOfIncorrectKeyValuePairType(int count) + { + ICollection collection = NonGenericICollectionFactory(count); + KeyValuePair[] array = new KeyValuePair[count * 3 / 2]; + AssertExtensions.Throws("array", null, () => collection.CopyTo(array, 0)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void ICollection_NonGeneric_CopyTo_ArrayOfCorrectKeyValuePairType(int count) + { + ICollection collection = NonGenericICollectionFactory(count); + KeyValuePair[] array = new KeyValuePair[count]; + collection.CopyTo(array, 0); + int i = 0; + foreach (object obj in collection) + { + Assert.Equal(array[i++], obj); + } + } + + #endregion + } +} diff --git a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj index ab0bc732cda1b..83a763207a85c 100644 --- a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj +++ b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj @@ -84,6 +84,12 @@ + + + + + + From a4dca876c6149b4632ba997c373a7dc8153ea0cd Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 12 Jun 2024 18:29:12 -0400 Subject: [PATCH 2/6] Open-code collection data structure rather than using Dictionary+List This rewrites the core of the type to be based on a custom data structure rather than wrapping Dictionary and List. The core structure is based on both Dictionary in corelib and the OrderedDictionary prototype in corefxlab. This also adds missing TryAdd, Capacity, EnsureCapacity, and TrimExcess members for parity with Dictionary, and fixes debugger views for the Key/ValueCollections. --- .../ref/System.Collections.cs | 4 + .../src/System.Collections.csproj | 25 +- .../Collections/Generic/OrderedDictionary.cs | 1130 +++++++++++++---- .../src/System/Collections/ThrowHelper.cs | 36 + .../OrderedDictionary.Generic.Tests.Keys.cs | 1 - .../OrderedDictionary.Generic.Tests.cs | 122 ++ .../OrderedDictionary.Generic.cs | 1 + .../OrderedDictionary.IList.Tests.cs | 1 - .../OrderedDictionary.Tests.cs | 2 +- 9 files changed, 1076 insertions(+), 246 deletions(-) create mode 100644 src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs 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] From bfdee8f3e538806e3186dc283a6efeb68b2e22b2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Jun 2024 11:14:02 -0400 Subject: [PATCH 3/6] Address PR feedback --- .../System/Collections/Generic/OrderedDictionary.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 284dfcf8b393e..8b52c11e0f41e 100644 --- a/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs +++ b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs @@ -111,7 +111,7 @@ public OrderedDictionary(int capacity, IEqualityComparer? comparer) _comparer = comparer ?? EqualityComparer.Default; if (typeof(TKey) == typeof(string) && - NonRandomizedStringEqualityComparer.GetStringComparer(_comparer!) is IEqualityComparer stringComparer) + NonRandomizedStringEqualityComparer.GetStringComparer(_comparer) is IEqualityComparer stringComparer) { _comparer = (IEqualityComparer)stringComparer; } @@ -361,7 +361,7 @@ public TValue this[TKey key] { ArgumentNullException.ThrowIfNull(key); - bool modified = TryInsert(-1, key, value, InsertionBehavior.OverwriteExisting); + bool modified = TryInsert(index: -1, key, value, InsertionBehavior.OverwriteExisting); Debug.Assert(modified); } } @@ -391,6 +391,7 @@ private bool TryInsert(int index, TKey key, TValue value, InsertionBehavior beha switch (behavior) { case InsertionBehavior.OverwriteExisting: + Debug.Assert(index < 0, "Expected index to be unspecied when overwriting an existing key."); _entries[i].Value = value; return true; @@ -399,6 +400,8 @@ private bool TryInsert(int index, TKey key, TValue value, InsertionBehavior beha break; default: + Debug.Assert(behavior is InsertionBehavior.IgnoreInsertion, $"Unknown behavior: {behavior}"); + Debug.Assert(index < 0, "Expected index to be unspecied when ignoring a duplicate key."); return false; } } @@ -460,7 +463,7 @@ public void Add(TKey key, TValue value) { ArgumentNullException.ThrowIfNull(key); - TryInsert(-1, key, value, InsertionBehavior.ThrowOnExisting); + TryInsert(index: -1, key, value, InsertionBehavior.ThrowOnExisting); } /// Adds the specified key and value to the dictionary if the key doesn't already exist. @@ -472,7 +475,7 @@ public bool TryAdd(TKey key, TValue value) { ArgumentNullException.ThrowIfNull(key); - return TryInsert(-1, key, value, InsertionBehavior.IgnoreInsertion); + return TryInsert(index: -1, key, value, InsertionBehavior.IgnoreInsertion); } /// Adds each element of the enumerable to the dictionary. From 737bad165bb6444fa87a9ba09cdc9261c5420a0c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Jun 2024 12:42:52 -0400 Subject: [PATCH 4/6] Add more tests based on code coverage gaps --- .../OutOfBoundsRegression.cs | 407 +++++++++++------- .../OrderedDictionary.Generic.Tests.cs | 19 +- .../OrderedDictionary.Generic.cs | 19 +- .../OrderedDictionary.IList.Tests.cs | 47 ++ .../OrderedDictionary.Tests.cs | 24 ++ 5 files changed, 345 insertions(+), 171 deletions(-) diff --git a/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs index c9088f393e839..6fe06dd432e74 100644 --- a/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs +++ b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs @@ -10,27 +10,21 @@ namespace System.Collections.Tests { - public class InternalHashCodeTests + #region Dictionary + public class InternalHashCodeTests_Dictionary_NullComparer : InternalHashCodeTests> { - private static Type nonRandomizedDefaultComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+DefaultComparer", throwOnError: true); - private static Type nonRandomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); - private static Type nonRandomizedOrdinalIgnoreCaseComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer", throwOnError: true); - private static Type randomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.RandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); - private static Type randomizedOrdinalIgnoreCaseComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.RandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer", throwOnError: true); + protected override Dictionary CreateCollection() => new Dictionary(); + protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; - /// - /// Given a byte array, copies it to the string, without messing with any encoding. This issue was hit on a x64 machine - /// - private static string GetString(byte[] bytes) - { - var chars = new char[bytes.Length / sizeof(char)]; - Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length); - return new string(chars); - } + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; [Fact] [OuterLoop("Takes over 55% of System.Collections.Tests testing time")] - public static void OutOfBoundsRegression() + public void OutOfBoundsRegression() { var dictionary = new Dictionary(); @@ -48,147 +42,225 @@ public static void OutOfBoundsRegression() } } - [Fact] - public static void ComparerImplementations_Dictionary_WithWellKnownStringComparers() + /// + /// Given a byte array, copies it to the string, without messing with any encoding. This issue was hit on a x64 machine + /// + private static string GetString(byte[] bytes) { - // null comparer - - RunDictionaryTest( - equalityComparer: null, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedDefaultComparerType, - expectedPublicComparerBeforeCollisionThreshold: EqualityComparer.Default, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // EqualityComparer.Default comparer - - RunDictionaryTest( - equalityComparer: EqualityComparer.Default, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedDefaultComparerType, - expectedPublicComparerBeforeCollisionThreshold: EqualityComparer.Default, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // Ordinal comparer - - RunDictionaryTest( - equalityComparer: StringComparer.Ordinal, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedOrdinalComparerType, - expectedPublicComparerBeforeCollisionThreshold: StringComparer.Ordinal, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // OrdinalIgnoreCase comparer - - RunDictionaryTest( - equalityComparer: StringComparer.OrdinalIgnoreCase, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedOrdinalIgnoreCaseComparerType, - expectedPublicComparerBeforeCollisionThreshold: StringComparer.OrdinalIgnoreCase, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalIgnoreCaseComparerType); - - // linguistic comparer (not optimized) - - RunDictionaryTest( - equalityComparer: StringComparer.InvariantCulture, - expectedInternalComparerTypeBeforeCollisionThreshold: StringComparer.InvariantCulture.GetType(), - expectedPublicComparerBeforeCollisionThreshold: StringComparer.InvariantCulture, - expectedInternalComparerTypeAfterCollisionThreshold: StringComparer.InvariantCulture.GetType()); - - // CollectionsMarshal.GetValueRefOrAddDefault - - RunCollectionTestCommon( - () => new Dictionary(StringComparer.Ordinal), - (dictionary, key) => CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _) = null, - (dictionary, key) => dictionary.ContainsKey(key), - dictionary => dictionary.Comparer, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedOrdinalComparerType, - expectedPublicComparerBeforeCollisionThreshold: StringComparer.Ordinal, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - static void RunDictionaryTest( - IEqualityComparer equalityComparer, - Type expectedInternalComparerTypeBeforeCollisionThreshold, - IEqualityComparer expectedPublicComparerBeforeCollisionThreshold, - Type expectedInternalComparerTypeAfterCollisionThreshold) - { - RunCollectionTestCommon( - () => new Dictionary(equalityComparer), - (dictionary, key) => dictionary.Add(key, null), - (dictionary, key) => dictionary.ContainsKey(key), - dictionary => dictionary.Comparer, - expectedInternalComparerTypeBeforeCollisionThreshold, - expectedPublicComparerBeforeCollisionThreshold, - expectedInternalComparerTypeAfterCollisionThreshold); - } + var chars = new char[bytes.Length / sizeof(char)]; + Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length); + return new string(chars); } + } - [Fact] - public static void ComparerImplementations_HashSet_WithWellKnownStringComparers() - { - // null comparer - - RunHashSetTest( - equalityComparer: null, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedDefaultComparerType, - expectedPublicComparerBeforeCollisionThreshold: EqualityComparer.Default, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // EqualityComparer.Default comparer - - RunHashSetTest( - equalityComparer: EqualityComparer.Default, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedDefaultComparerType, - expectedPublicComparerBeforeCollisionThreshold: EqualityComparer.Default, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // Ordinal comparer - - RunHashSetTest( - equalityComparer: StringComparer.Ordinal, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedOrdinalComparerType, - expectedPublicComparerBeforeCollisionThreshold: StringComparer.Ordinal, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalComparerType); - - // OrdinalIgnoreCase comparer - - RunHashSetTest( - equalityComparer: StringComparer.OrdinalIgnoreCase, - expectedInternalComparerTypeBeforeCollisionThreshold: nonRandomizedOrdinalIgnoreCaseComparerType, - expectedPublicComparerBeforeCollisionThreshold: StringComparer.OrdinalIgnoreCase, - expectedInternalComparerTypeAfterCollisionThreshold: randomizedOrdinalIgnoreCaseComparerType); - - // linguistic comparer (not optimized) - - RunHashSetTest( - equalityComparer: StringComparer.InvariantCulture, - expectedInternalComparerTypeBeforeCollisionThreshold: StringComparer.InvariantCulture.GetType(), - expectedPublicComparerBeforeCollisionThreshold: StringComparer.InvariantCulture, - expectedInternalComparerTypeAfterCollisionThreshold: StringComparer.InvariantCulture.GetType()); - - static void RunHashSetTest( - IEqualityComparer equalityComparer, - Type expectedInternalComparerTypeBeforeCollisionThreshold, - IEqualityComparer expectedPublicComparerBeforeCollisionThreshold, - Type expectedInternalComparerTypeAfterCollisionThreshold) - { - RunCollectionTestCommon( - () => new HashSet(equalityComparer), - (set, key) => Assert.True(set.Add(key)), - (set, key) => set.Contains(key), - set => set.Comparer, - expectedInternalComparerTypeBeforeCollisionThreshold, - expectedPublicComparerBeforeCollisionThreshold, - expectedInternalComparerTypeAfterCollisionThreshold); - } - } + public class InternalHashCodeTests_Dictionary_DefaultComparer : InternalHashCodeTests> + { + protected override Dictionary CreateCollection() => new Dictionary(EqualityComparer.Default); + protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + + public class InternalHashCodeTests_Dictionary_OrdinalComparer : InternalHashCodeTests> + { + protected override Dictionary CreateCollection() => new Dictionary(StringComparer.Ordinal); + protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.Ordinal; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + + public class InternalHashCodeTests_Dictionary_OrdinalIgnoreCaseComparer : InternalHashCodeTests> + { + protected override Dictionary CreateCollection() => new Dictionary(StringComparer.OrdinalIgnoreCase); + protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalIgnoreCaseComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.OrdinalIgnoreCase; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalIgnoreCaseComparerType; + } + + public class InternalHashCodeTests_Dictionary_LinguisticComparer : InternalHashCodeTests> // (not optimized) + { + protected override Dictionary CreateCollection() => new Dictionary(StringComparer.InvariantCulture); + protected override void AddKey(Dictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => StringComparer.InvariantCulture.GetType(); + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.InvariantCulture; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => StringComparer.InvariantCulture.GetType(); + } + + + + public class InternalHashCodeTests_Dictionary_GetValueRefOrAddDefault : InternalHashCodeTests> + { + protected override Dictionary CreateCollection() => new Dictionary(StringComparer.Ordinal); + protected override void AddKey(Dictionary collection, string key) => CollectionsMarshal.GetValueRefOrAddDefault(collection, key, out _) = null; + protected override bool ContainsKey(Dictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(Dictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.Ordinal; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + #endregion + + #region HashSet + public class InternalHashCodeTests_HashSet_NullComparer : InternalHashCodeTests> + { + protected override HashSet CreateCollection() => new HashSet(); + protected override void AddKey(HashSet collection, string key) => collection.Add(key); + protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); + protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + + public class InternalHashCodeTests_HashSet_DefaultComparer : InternalHashCodeTests> + { + protected override HashSet CreateCollection() => new HashSet(EqualityComparer.Default); + protected override void AddKey(HashSet collection, string key) => collection.Add(key); + protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); + protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + + public class InternalHashCodeTests_HashSet_OrdinalComparer : InternalHashCodeTests> + { + protected override HashSet CreateCollection() => new HashSet(StringComparer.Ordinal); + protected override void AddKey(HashSet collection, string key) => collection.Add(key); + protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); + protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.Ordinal; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalComparerType; + } + + public class InternalHashCodeTests_HashSet_OrdinalIgnoreCaseComparer : InternalHashCodeTests> + { + protected override HashSet CreateCollection() => new HashSet(StringComparer.OrdinalIgnoreCase); + protected override void AddKey(HashSet collection, string key) => collection.Add(key); + protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); + protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalIgnoreCaseComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.OrdinalIgnoreCase; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => randomizedOrdinalIgnoreCaseComparerType; + } + + public class InternalHashCodeTests_HashSet_LinguisticComparer : InternalHashCodeTests> // (not optimized) + { + protected override HashSet CreateCollection() => new HashSet(StringComparer.InvariantCulture); + protected override void AddKey(HashSet collection, string key) => collection.Add(key); + protected override bool ContainsKey(HashSet collection, string key) => collection.Contains(key); + protected override IEqualityComparer GetComparer(HashSet collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => StringComparer.InvariantCulture.GetType(); + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.InvariantCulture; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => StringComparer.InvariantCulture.GetType(); + } + #endregion + + #region OrderedDictionary + public class InternalHashCodeTests_OrderedDictionary_NullComparer : InternalHashCodeTests> + { + protected override OrderedDictionary CreateCollection() => new OrderedDictionary(); + protected override void AddKey(OrderedDictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => EqualityComparer.Default.GetType(); + } - private static void RunCollectionTestCommon( - Func collectionFactory, - Action addKeyCallback, - Func containsKeyCallback, - Func> getComparerCallback, - Type expectedInternalComparerTypeBeforeCollisionThreshold, - IEqualityComparer expectedPublicComparerBeforeCollisionThreshold, - Type expectedInternalComparerTypeAfterCollisionThreshold) + public class InternalHashCodeTests_OrderedDictionary_DefaultComparer : InternalHashCodeTests> + { + protected override OrderedDictionary CreateCollection() => new OrderedDictionary(EqualityComparer.Default); + protected override void AddKey(OrderedDictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedDefaultComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => EqualityComparer.Default; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => EqualityComparer.Default.GetType(); + } + + public class InternalHashCodeTests_OrderedDictionary_OrdinalComparer : InternalHashCodeTests> + { + protected override OrderedDictionary CreateCollection() => new OrderedDictionary(StringComparer.Ordinal); + protected override void AddKey(OrderedDictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.Ordinal; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => StringComparer.Ordinal.GetType(); + } + + public class InternalHashCodeTests_OrderedDictionary_OrdinalIgnoreCaseComparer : InternalHashCodeTests> + { + protected override OrderedDictionary CreateCollection() => new OrderedDictionary(StringComparer.OrdinalIgnoreCase); + protected override void AddKey(OrderedDictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => nonRandomizedOrdinalIgnoreCaseComparerType; + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.OrdinalIgnoreCase; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => StringComparer.OrdinalIgnoreCase.GetType(); + } + + public class InternalHashCodeTests_OrderedDictionary_LinguisticComparer : InternalHashCodeTests> // (not optimized) + { + protected override OrderedDictionary CreateCollection() => new OrderedDictionary(StringComparer.InvariantCulture); + protected override void AddKey(OrderedDictionary collection, string key) => collection.Add(key, key); + protected override bool ContainsKey(OrderedDictionary collection, string key) => collection.ContainsKey(key); + protected override IEqualityComparer GetComparer(OrderedDictionary collection) => collection.Comparer; + + protected override Type ExpectedInternalComparerTypeBeforeCollisionThreshold => StringComparer.InvariantCulture.GetType(); + protected override IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold => StringComparer.InvariantCulture; + protected override Type ExpectedInternalComparerTypeAfterCollisionThreshold => StringComparer.InvariantCulture.GetType(); + } + #endregion + + public abstract class InternalHashCodeTests + { + protected static Type nonRandomizedDefaultComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+DefaultComparer", throwOnError: true); + protected static Type nonRandomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); + protected static Type nonRandomizedOrdinalIgnoreCaseComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer", throwOnError: true); + protected static Type randomizedOrdinalComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.RandomizedStringEqualityComparer+OrdinalComparer", throwOnError: true); + protected static Type randomizedOrdinalIgnoreCaseComparerType = typeof(object).Assembly.GetType("System.Collections.Generic.RandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer", throwOnError: true); + + protected abstract TCollection CreateCollection(); + protected abstract void AddKey(TCollection collection, string key); + protected abstract bool ContainsKey(TCollection collection, string key); + protected abstract IEqualityComparer GetComparer(TCollection collection); + + protected abstract Type ExpectedInternalComparerTypeBeforeCollisionThreshold { get; } + protected abstract IEqualityComparer ExpectedPublicComparerBeforeCollisionThreshold { get; } + protected abstract Type ExpectedInternalComparerTypeAfterCollisionThreshold { get; } + + [Fact] + public void ComparerImplementations_Dictionary_WithWellKnownStringComparers() { - TCollection collection = collectionFactory(); + TCollection collection = CreateCollection(); List allKeys = new List(); // First, go right up to the collision threshold, but don't exceed it. @@ -196,7 +268,7 @@ private static void RunCollectionTestCommon( for (int i = 0; i < 100; i++) { string newKey = _collidingStrings[i]; - addKeyCallback(collection, newKey); + AddKey(collection, newKey); allKeys.Add(newKey); } @@ -204,10 +276,10 @@ private static void RunCollectionTestCommon( Assert.NotNull(internalComparerField); IEqualityComparer actualInternalComparerBeforeCollisionThreshold = (IEqualityComparer)internalComparerField.GetValue(collection); - ValidateBehaviorOfInternalComparerVsPublicComparer(actualInternalComparerBeforeCollisionThreshold, expectedPublicComparerBeforeCollisionThreshold); + ValidateBehaviorOfInternalComparerVsPublicComparer(actualInternalComparerBeforeCollisionThreshold, ExpectedPublicComparerBeforeCollisionThreshold); - Assert.Equal(expectedInternalComparerTypeBeforeCollisionThreshold, actualInternalComparerBeforeCollisionThreshold?.GetType()); - Assert.Equal(expectedPublicComparerBeforeCollisionThreshold, getComparerCallback(collection)); + Assert.Equal(ExpectedInternalComparerTypeBeforeCollisionThreshold, actualInternalComparerBeforeCollisionThreshold?.GetType()); + Assert.Equal(ExpectedPublicComparerBeforeCollisionThreshold, GetComparer(collection)); // Now exceed the collision threshold, which should rebucket entries. // Continue adding a few more entries to ensure we didn't corrupt internal state. @@ -218,31 +290,34 @@ private static void RunCollectionTestCommon( Assert.Equal(0, _lazyGetNonRandomizedHashCodeDel.Value(newKey)); // ensure has a zero hash code Ordinal Assert.Equal(0x24716ca0, _lazyGetNonRandomizedOrdinalIgnoreCaseHashCodeDel.Value(newKey)); // ensure has a zero hash code OrdinalIgnoreCase - addKeyCallback(collection, newKey); + AddKey(collection, newKey); allKeys.Add(newKey); } IEqualityComparer actualInternalComparerAfterCollisionThreshold = (IEqualityComparer)internalComparerField.GetValue(collection); - ValidateBehaviorOfInternalComparerVsPublicComparer(actualInternalComparerAfterCollisionThreshold, expectedPublicComparerBeforeCollisionThreshold); + ValidateBehaviorOfInternalComparerVsPublicComparer(actualInternalComparerAfterCollisionThreshold, ExpectedPublicComparerBeforeCollisionThreshold); - Assert.Equal(expectedInternalComparerTypeAfterCollisionThreshold, actualInternalComparerAfterCollisionThreshold?.GetType()); - Assert.Equal(expectedPublicComparerBeforeCollisionThreshold, getComparerCallback(collection)); // shouldn't change this return value after collision threshold met + Assert.Equal(ExpectedInternalComparerTypeAfterCollisionThreshold, actualInternalComparerAfterCollisionThreshold?.GetType()); + Assert.Equal(ExpectedPublicComparerBeforeCollisionThreshold, GetComparer(collection)); // shouldn't change this return value after collision threshold met // And validate that all strings are present in the dictionary. foreach (string key in allKeys) { - Assert.True(containsKeyCallback(collection, key)); + Assert.True(ContainsKey(collection, key)); } // Also make sure we didn't accidentally put the internal comparer in the serialized object data. - collection = collectionFactory(); - SerializationInfo si = new SerializationInfo(collection.GetType(), new FormatterConverter()); - ((ISerializable)collection).GetObjectData(si, new StreamingContext()); + collection = CreateCollection(); + if (collection is ISerializable) + { + SerializationInfo si = new SerializationInfo(collection.GetType(), new FormatterConverter()); + ((ISerializable)collection).GetObjectData(si, new StreamingContext()); - object serializedComparer = si.GetValue("Comparer", typeof(IEqualityComparer)); - Assert.Equal(expectedPublicComparerBeforeCollisionThreshold, serializedComparer); + object serializedComparer = si.GetValue("Comparer", typeof(IEqualityComparer)); + Assert.Equal(ExpectedPublicComparerBeforeCollisionThreshold, serializedComparer); + } } private static Lazy> _lazyGetNonRandomizedHashCodeDel = new Lazy>( 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 c427c6c4f48ce..84d5e0341f8ba 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 @@ -58,6 +58,14 @@ public void OrderedDictionary_Generic_Constructor() Assert.Empty(instance.Values); Assert.Same(comparer, instance.Comparer); Assert.InRange(instance.Capacity, 42, int.MaxValue); + + IEqualityComparer customComparer = EqualityComparer.Create(comparer.Equals, comparer.GetHashCode); + instance = new OrderedDictionary(42, customComparer); + Assert.Empty(instance); + Assert.Empty(instance.Keys); + Assert.Empty(instance.Values); + Assert.Same(customComparer, instance.Comparer); + Assert.InRange(instance.Capacity, 42, int.MaxValue); } [Theory] @@ -355,14 +363,17 @@ public void OrderedDictionary_Generic_TrimExcess(int count) public void OrderedDictionary_Generic_EnsureCapacity() { OrderedDictionary dictionary = (OrderedDictionary)GenericIDictionaryFactory(); - Assert.Equal(0, dictionary.Capacity); - for (int i = 0; i < 10; i++) + + dictionary.EnsureCapacity(1); + Assert.InRange(dictionary.Capacity, 1, int.MaxValue); + + for (int i = 0; i < 30; i++) { dictionary.TryAdd(CreateTKey(i), CreateTValue(i)); } int count = dictionary.Count; - Assert.InRange(count, 1, 10); + Assert.InRange(count, 1, 30); 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)); @@ -374,7 +385,7 @@ public void OrderedDictionary_Generic_EnsureCapacity() Assert.Equal(newCapacity, dictionary.Capacity); Assert.InRange(newCapacity, oldCapacity * 2, int.MaxValue); - for (int i = 0; i < 10; i++) + for (int i = 0; i < 30; i++) { Assert.True(dictionary.ContainsKey(CreateTKey(i))); } 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 f87548107b0d6..7a56dfb3dfb33 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs @@ -9,7 +9,7 @@ namespace System.Collections.Tests public class OrderedDictionary_Generic_Tests_string_string : OrderedDictionary_Generic_Tests { protected override KeyValuePair CreateT(int seed) => - new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + new KeyValuePair(CreateTKey(seed), CreateTValue(seed + 500)); protected override string CreateTKey(int seed) { @@ -23,6 +23,23 @@ protected override string CreateTKey(int seed) protected override string CreateTValue(int seed) => CreateTKey(seed); } + public class OrderedDictionary_Generic_Tests_object_byte : OrderedDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) => + new KeyValuePair(CreateTKey(seed), CreateTValue(seed + 500)); + + protected override object CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override byte CreateTValue(int seed) => (byte)new Random(seed).Next(); + } + public class OrderedDictionary_Generic_Tests_int_int : OrderedDictionary_Generic_Tests { protected override bool DefaultValueAllowed { get { return true; } } 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 e9fd08158be59..2b68682dd4e59 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Xunit; namespace System.Collections.Tests { @@ -28,6 +29,21 @@ private string CreateString(int seed) rand.NextBytes(bytes1); return Convert.ToBase64String(bytes1); } + + [Fact] + public void IList_Generic_IndexOfRequiresValueMatch() + { + var dictionary = new OrderedDictionary() + { + ["a"] = "1", + ["b"] = "2", + ["c"] = "3" + }; + + KeyValuePair pair = dictionary.GetAt(2); + Assert.Equal(2, ((IList>)dictionary).IndexOf(pair)); + Assert.Equal(-1, ((IList>)dictionary).IndexOf(new KeyValuePair(pair.Key, "d"))); + } } public class OrderedDictionary_IList_NonGeneric_Tests : IList_NonGeneric_Tests @@ -55,6 +71,37 @@ private string CreateString(int seed) rand.NextBytes(bytes1); return Convert.ToBase64String(bytes1); } + + [Fact] + public void Indexer_Set_WrongType_ThrowsException() + { + IList list = NonGenericIListFactory(); + list.Add(new KeyValuePair("key", "value")); + AssertExtensions.Throws("value", () => list[0] = new KeyValuePair(42, 42)); + AssertExtensions.Throws("value", () => list[0] = "key"); + } + + [Fact] + public void Add_WrongType_ThrowsException() + { + IList list = NonGenericIListFactory(); + list.Add(KeyValuePair.Create("key", "value")); + AssertExtensions.Throws("value", () => list.Add(new KeyValuePair(42, 42))); + AssertExtensions.Throws("value", () => list.Add(new KeyValuePair("42", 42))); + AssertExtensions.Throws("value", () => list.Add(42)); + Assert.Equal(1, list.Count); + } + + [Fact] + public void Insert_WrongType_ThrowsException() + { + IList list = NonGenericIListFactory(); + list.Insert(0, KeyValuePair.Create("key", "value")); + AssertExtensions.Throws("value", () => list.Insert(0, new KeyValuePair(42, 42))); + AssertExtensions.Throws("value", () => list.Insert(0, new KeyValuePair("42", 42))); + AssertExtensions.Throws("value", () => list.Insert(0, 42)); + Assert.Equal(1, list.Count); + } } public class OrderedDictionary_Keys_IList_Generic_Tests : IList_Generic_Tests 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 e5af9af5259c0..6f953665f4bea 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs @@ -105,6 +105,15 @@ public void IDictionary_NonGeneric_ItemSet_NullValueWhenDefaultValueIsNonNull() Assert.Throws(() => dictionary[GetNewKey(dictionary)] = null); } + [Fact] + public void IDictionary_NonGeneric_ItemGet_KeyOfWrongType() + { + IDictionary dictionary = new OrderedDictionary(); + dictionary.Add("key", "value"); + Assert.Null(dictionary[42]); + Assert.Null(dictionary[KeyValuePair.Create("key", "value")]); + } + [Fact] public void IDictionary_NonGeneric_ItemSet_KeyOfWrongType() { @@ -137,6 +146,10 @@ public void IDictionary_NonGeneric_Add_KeyOfWrongType() object missingKey = 23; AssertExtensions.Throws("key", () => dictionary.Add(missingKey, CreateTValue(12345))); Assert.Empty(dictionary); + + dictionary = new OrderedDictionary(); + AssertExtensions.Throws("value", () => dictionary.Add("key", null)); + Assert.Empty(dictionary); } } @@ -174,6 +187,17 @@ public void IDictionary_NonGeneric_Contains_KeyOfWrongType() } } + [Fact] + public void IDictionary_NonGeneric_Remove_KeyOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + dictionary.Remove(1); // ignored + Assert.Empty(dictionary); + } + } + [Fact] public void CantAcceptDuplicateKeysFromSourceDictionary() { From c199c738b86267f358fdb945d8704ee795ff9cf8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Jun 2024 12:44:17 -0400 Subject: [PATCH 5/6] Address PR feedback --- .../System/Collections/Generic/OrderedDictionary.cs | 10 +++++----- .../src/System/Collections/ThrowHelper.cs | 2 +- .../OrderedDictionary.Generic.Tests.Keys.cs | 6 +++--- .../OrderedDictionary.Generic.Tests.Values.cs | 4 ++-- .../OrderedDictionary/OrderedDictionary.IList.Tests.cs | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) 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 8b52c11e0f41e..cb8b2008fa4f4 100644 --- a/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs +++ b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs @@ -564,7 +564,7 @@ public KeyValuePair GetAt(int index) { if ((uint)index >= (uint)_count) { - ThrowHelper.ThrowIndexOutOfRange(); + ThrowHelper.ThrowIndexArgumentOutOfRange(); } Debug.Assert(_entries is not null, "count must be positive, which means we must have entries"); @@ -693,7 +693,7 @@ public void Insert(int index, TKey key, TValue value) { if ((uint)index > (uint)_count) { - ThrowHelper.ThrowIndexOutOfRange(); + ThrowHelper.ThrowIndexArgumentOutOfRange(); } ArgumentNullException.ThrowIfNull(key); @@ -738,7 +738,7 @@ public void RemoveAt(int index) int count = _count; if ((uint)index >= (uint)count) { - ThrowHelper.ThrowIndexOutOfRange(); + ThrowHelper.ThrowIndexArgumentOutOfRange(); } // Remove from the associated bucket chain the entry that lives at the specified index. @@ -764,7 +764,7 @@ public void SetAt(int index, TValue value) { if ((uint)index >= (uint)_count) { - ThrowHelper.ThrowIndexOutOfRange(); + ThrowHelper.ThrowIndexArgumentOutOfRange(); } Debug.Assert(_entries is not null); @@ -781,7 +781,7 @@ public void SetAt(int index, TKey key, TValue value) { if ((uint)index >= (uint)_count) { - ThrowHelper.ThrowIndexOutOfRange(); + ThrowHelper.ThrowIndexArgumentOutOfRange(); } ArgumentNullException.ThrowIfNull(key); diff --git a/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs b/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs index f1c9bc60e90fd..1f40b6185ce5c 100644 --- a/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs +++ b/src/libraries/System.Collections/src/System/Collections/ThrowHelper.cs @@ -25,7 +25,7 @@ internal static void ThrowConcurrentOperation() => /// Throws an exception for an index being out of range. [DoesNotReturn] - internal static void ThrowIndexOutOfRange() => + internal static void ThrowIndexArgumentOutOfRange() => throw new ArgumentOutOfRangeException("index"); /// Throws an exception for a version check failing during enumeration. 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 23ba13609377a..3f3e2439a7560 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 @@ -19,7 +19,7 @@ public class OrderedDictionary_Generic_Tests_Keys : ICollection_Generic_Tests GenericICollectionFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 13453; for (int i = 0; i < count; i++) { @@ -42,7 +42,7 @@ protected override string CreateT(int seed) [MemberData(nameof(ValidCollectionSizes))] public void OrderedDictionary_Generic_KeyCollection_GetEnumerator(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 13453; while (dictionary.Count < count) { @@ -67,7 +67,7 @@ public class OrderedDictionary_Generic_Tests_Keys_AsICollection : ICollection_No protected override ICollection NonGenericICollectionFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 13453; for (int i = 0; i < count; i++) { diff --git a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs index d853f5b24e06c..2428599e0f9f7 100644 --- a/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Values.cs @@ -21,7 +21,7 @@ public class OrderedDictionary_Generic_Tests_Values : ICollection_Generic_Tests< protected override ICollection GenericICollectionFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 12453; for (int i = 0; i < count; i++) @@ -45,7 +45,7 @@ protected override string CreateT(int seed) [MemberData(nameof(ValidCollectionSizes))] public void OrderedDictionary_Generic_ValueCollection_GetEnumerator(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 12453; while (dictionary.Count < count) 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 2b68682dd4e59..0c42ad055bc17 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 @@ -121,7 +121,7 @@ public class OrderedDictionary_Keys_IList_Generic_Tests : IList_Generic_Tests GenericIListFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 42; while (dictionary.Count < count) @@ -164,7 +164,7 @@ protected override object CreateT(int seed) => protected override IList NonGenericIListFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 42; while (dictionary.Count < count) @@ -203,7 +203,7 @@ public class OrderedDictionary_Values_IList_Generic_Tests : IList_Generic_Tests< protected override IList GenericIListFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 42; while (dictionary.Count < count) @@ -244,7 +244,7 @@ protected override object CreateT(int seed) => protected override IList NonGenericIListFactory(int count) { - OrderedDictionary dictionary = new OrderedDictionary(); + OrderedDictionary dictionary = new(); int seed = 42; while (dictionary.Count < count) From aacd413d37e75a800db05ca7756652c359dedd27 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Jun 2024 15:06:50 -0400 Subject: [PATCH 6/6] Try to fix NativeAOT tests in CI --- src/libraries/System.Collections/tests/default.rd.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libraries/System.Collections/tests/default.rd.xml b/src/libraries/System.Collections/tests/default.rd.xml index cff6d074fe9ae..5a705d2b4c11a 100644 --- a/src/libraries/System.Collections/tests/default.rd.xml +++ b/src/libraries/System.Collections/tests/default.rd.xml @@ -151,6 +151,11 @@ + + + + +