diff --git a/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs b/src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs index f73a2d3193f32f..00b05b11cc7596 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 adca2780d96d5a..3ce765467e5129 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 a441cf4d787330..b4877c902d07fc 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 44f67e0ca24f83..811ac67950ac0c 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 b823b530b33839..2b88b3ef7561f0 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 70f4abe1a4bf0d..3625b253780b12 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -106,6 +106,175 @@ 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 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 { } } + 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 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; } + 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 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 + { + 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 871c508e84104a..ba91a4f329133a 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -10,15 +10,13 @@ - - - - + + + + + + @@ -31,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 new file mode 100644 index 00000000000000..cb8b2008fa4f45 --- /dev/null +++ b/src/libraries/System.Collections/src/System/Collections/Generic/OrderedDictionary.cs @@ -0,0 +1,1867 @@ +// 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 +{ + /// + /// 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 : + IDictionary, IReadOnlyDictionary, IDictionary, + IList>, IReadOnlyList>, IList + where TKey : notnull + { + /// 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; + /// 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() : this(0, null) + { + } + + /// + /// 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) : this(capacity, null) + { + } + + /// + /// 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) : this(0, comparer) + { + } + + /// + /// 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) + { + 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; + } + } + + /// + /// 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) : this(dictionary, null) + { + } + + /// + /// 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) : + this(dictionary?.Count ?? 0, comparer) + { + ArgumentNullException.ThrowIfNull(dictionary); + + 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) : this(collection, null) + { + } + + /// + /// 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) : + this((collection as ICollection>)?.Count ?? 0, comparer) + { + ArgumentNullException.ThrowIfNull(collection); + + 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 + { + 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 => _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 => this; + + /// + 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 + { + if (!TryGetValue(key, out TValue? value)) + { + ThrowHelper.ThrowKeyNotFound(key); + } + + return value; + } + set + { + ArgumentNullException.ThrowIfNull(key); + + bool modified = TryInsert(index: -1, key, value, InsertionBehavior.OverwriteExisting); + Debug.Assert(modified); + } + } + + /// 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) + { + case InsertionBehavior.OverwriteExisting: + Debug.Assert(index < 0, "Expected index to be unspecied when overwriting an existing key."); + _entries[i].Value = value; + return true; + + case InsertionBehavior.ThrowOnExisting: + ThrowHelper.ThrowDuplicateKey(key); + 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; + } + } + + // 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. + /// 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) + { + ArgumentNullException.ThrowIfNull(key); + + TryInsert(index: -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(index: -1, key, value, InsertionBehavior.IgnoreInsertion); + } + + /// 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() + { + 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) => 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) + { + 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. + /// The element at the specified index. + /// is less than 0 or greater than or equal to . + public KeyValuePair GetAt(int index) + { + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexArgumentOutOfRange(); + } + + 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 . + /// The key to locate. + /// The index of if found; otherwise, -1. + /// is null. + public int IndexOf(TKey key) + { + ArgumentNullException.ThrowIfNull(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. + /// 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)_count) + { + ThrowHelper.ThrowIndexArgumentOutOfRange(); + } + + 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) => 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. + /// The removed element. + /// true if the element is successfully found and removed; otherwise, false. + public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) + { + ArgumentNullException.ThrowIfNull(key); + + // Find the key. + int index = IndexOf(key); + if (index >= 0) + { + // It exists. Remove it. + Debug.Assert(_entries is not null); + + value = _entries[index].Value; + RemoveAt(index); + + return true; + } + + value = default; + 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) + { + int count = _count; + if ((uint)index >= (uint)count) + { + ThrowHelper.ThrowIndexArgumentOutOfRange(); + } + + // 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) + { + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexArgumentOutOfRange(); + } + + 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. + /// 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) + { + if ((uint)index >= (uint)_count) + { + ThrowHelper.ThrowIndexArgumentOutOfRange(); + } + + 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; + } + } + + // 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) + { + ThrowHelper.ThrowDuplicateKey(key); + } + + // 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)); + } + + _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() => 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) + { + 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. + /// 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) + { + ArgumentNullException.ThrowIfNull(key); + + // 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) + { + // 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); + + // 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>() : + 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)); + + int index = IndexOf(item.Key); + if (index >= 0) + { + Debug.Assert(_entries is not null); + if (EqualityComparer.Default.Equals(item.Value, _entries[index].Value)) + { + return index; + } + } + + 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 + 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); + } + + for (int i = 0; i < _count; i++) + { + ref Entry entry = ref _entries![i]; + array[arrayIndex++] = new(entry.Key, entry.Value); + } + } + + /// + 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) 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)); + } + + 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 < _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 && + 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); + } + } + + /// Represents a key/value pair in the dictionary. + private struct Entry + { + /// 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; + /// A snapshot of the dictionary's version when enumeration began. + private readonly int _version; + /// Whether Current should be a DictionaryEntry. + private readonly bool _useDictionaryEntry; + /// The current index. + private int _index; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary, bool useDictionaryEntry) + { + _dictionary = dictionary; + _version = _dictionary._version; + _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() + { + OrderedDictionary dictionary = _dictionary; + + if (_version != dictionary._version) + { + 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; + return false; + } + + /// + 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. + 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) + { + 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) + { + 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.GetAt(index).Key; + set => throw new NotSupportedException(); + } + + /// + object? IList.this[int index] + { + get => _dictionary.GetAt(index).Key; + set => throw new NotSupportedException(); + } + + /// + TKey IReadOnlyList.this[int index] => _dictionary.GetAt(index).Key; + + /// 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's enumerator. + private OrderedDictionary.Enumerator _enumerator; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary) => _enumerator = dictionary.GetEnumerator(); + + /// + public TKey Current => _enumerator.Current.Key; + + /// + object IEnumerator.Current => Current; + + /// + public bool MoveNext() => _enumerator.MoveNext(); + + /// + void IEnumerator.Reset() => EnumerableHelpers.Reset(ref _enumerator); + + /// + 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. + 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); + + 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].Value; + } + } + + /// Returns an enumerator that iterates through the . + /// A for the . + public Enumerator GetEnumerator() => new(_dictionary); + + /// + TValue IList.this[int index] + { + get => _dictionary.GetAt(index).Value; + set => throw new NotSupportedException(); + } + + /// + TValue IReadOnlyList.this[int index] => _dictionary.GetAt(index).Value; + + /// + object? IList.this[int index] + { + get => _dictionary.GetAt(index).Value; + set => throw new NotSupportedException(); + } + + /// + bool ICollection.Contains(TValue item) => _dictionary.ContainsValue(item); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? EnumerableHelpers.GetEmptyEnumerator() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); + + /// + int IList.IndexOf(TValue item) + { + Entry[]? entries = _dictionary._entries; + if (entries is not null) + { + int count = _dictionary._count; + for (int i = 0; i < count; i++) + { + if (EqualityComparer.Default.Equals(item, entries[i].Value)) + { + 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) + { + Entry[]? entries = _dictionary._entries; + if (entries is not null) + { + int count = _dictionary._count; + + if (value is null && default(TValue) is null) + { + for (int i = 0; i < count; i++) + { + if (entries[i].Value is null) + { + return i; + } + } + } + else if (value is TValue tvalue) + { + for (int i = 0; i < count; i++) + { + if (EqualityComparer.Default.Equals(tvalue, entries[i].Value)) + { + 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's enumerator. + private OrderedDictionary.Enumerator _enumerator; + + /// Initialize the enumerator. + internal Enumerator(OrderedDictionary dictionary) => _enumerator = dictionary.GetEnumerator(); + + /// + public TValue Current => _enumerator.Current.Value; + + /// + object? IEnumerator.Current => Current; + + /// + public bool MoveNext() => _enumerator.MoveNext(); + + /// + 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 00000000000000..1f40b6185ce5c7 --- /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 ThrowIndexArgumentOutOfRange() => + 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/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs b/src/libraries/System.Collections/tests/Generic/Dictionary/HashCollisionScenarios/OutOfBoundsRegression.cs index c9088f393e839c..6fe06dd432e743 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.Keys.cs b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs new file mode 100644 index 00000000000000..3f3e2439a75608 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.Keys.cs @@ -0,0 +1,105 @@ +// 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(); + 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(); + 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 ICollection NonGenericICollectionFactory(int count) + { + OrderedDictionary dictionary = new(); + 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 00000000000000..2428599e0f9f7c --- /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(); + + 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(); + + 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 00000000000000..84d5e0341f8baf --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.Tests.cs @@ -0,0 +1,421 @@ +// 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); + 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); + + 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] + [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); + 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); + } + } + + [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)); + } + + [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 + + [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; + 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))); + } + + [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); + 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); + + 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, 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)); + 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 < 30; i++) + { + Assert.True(dictionary.ContainsKey(CreateTKey(i))); + } + Assert.Equal(count, 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 00000000000000..7a56dfb3dfb33c --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Generic.cs @@ -0,0 +1,78 @@ +// 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), CreateTValue(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_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; } } + + 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 00000000000000..0c42ad055bc17e --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.IList.Tests.cs @@ -0,0 +1,271 @@ +// 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 +{ + 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); + } + + [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 + { + 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); + } + + [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 + { + 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(); + + 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 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(); + + 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(); + + 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(); + + 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 00000000000000..6f953665f4bea7 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/OrderedDictionary/OrderedDictionary.Tests.cs @@ -0,0 +1,249 @@ +// 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_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() + { + 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); + + dictionary = new OrderedDictionary(); + AssertExtensions.Throws("value", () => dictionary.Add("key", null)); + 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 IDictionary_NonGeneric_Remove_KeyOfWrongType() + { + if (!IsReadOnly) + { + IDictionary dictionary = new OrderedDictionary(); + dictionary.Remove(1); // ignored + Assert.Empty(dictionary); + } + } + + [Fact] + public void CantAcceptDuplicateKeysFromSourceDictionary() + { + Dictionary source = new Dictionary { { "a", 1 }, { "A", 1 } }; + AssertExtensions.Throws("key", () => 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 ab0bc732cda1b2..83a763207a85c6 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 @@ + + + + + + diff --git a/src/libraries/System.Collections/tests/default.rd.xml b/src/libraries/System.Collections/tests/default.rd.xml index cff6d074fe9ae6..5a705d2b4c11a5 100644 --- a/src/libraries/System.Collections/tests/default.rd.xml +++ b/src/libraries/System.Collections/tests/default.rd.xml @@ -151,6 +151,11 @@ + + + + +