Skip to content

Commit 2b0e517

Browse files
authored
Add OrderedDictionary (#103309)
* Add OrderedDictionary * Open-code collection data structure rather than using Dictionary+List This rewrites the core of the type to be based on a custom data structure rather than wrapping Dictionary and List. The core structure is based on both Dictionary in corelib and the OrderedDictionary prototype in corefxlab. This also adds missing TryAdd, Capacity, EnsureCapacity, and TrimExcess members for parity with Dictionary, and fixes debugger views for the Key/ValueCollections. * Address PR feedback * Add more tests based on code coverage gaps * Address PR feedback * Try to fix NativeAOT tests in CI
1 parent 48c8805 commit 2b0e517

18 files changed

+3628
-191
lines changed

src/libraries/Common/src/System/Collections/Generic/EnumerableHelpers.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ namespace System.Collections.Generic
88
/// </summary>
99
internal static partial class EnumerableHelpers
1010
{
11+
/// <summary>Calls Reset on an enumerator instance.</summary>
12+
/// <remarks>Enables Reset to be called without boxing on a struct enumerator that lacks a public Reset.</remarks>
13+
internal static void Reset<T>(ref T enumerator) where T : IEnumerator => enumerator.Reset();
14+
1115
/// <summary>Gets an enumerator singleton for an empty collection.</summary>
1216
internal static IEnumerator<T> GetEmptyEnumerator<T>() =>
1317
((IEnumerable<T>)Array.Empty<T>()).GetEnumerator();

src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,6 @@ public virtual void IDictionary_NonGeneric_Values_Enumeration_ParentDictionaryMo
491491
{
492492
Assert.Throws<InvalidOperationException>(() => valuesEnum.MoveNext());
493493
Assert.Throws<InvalidOperationException>(() => valuesEnum.Reset());
494-
Assert.Throws<InvalidOperationException>(() => valuesEnum.Current);
495494
}
496495
else
497496
{
@@ -832,7 +831,7 @@ public virtual void IDictionary_NonGeneric_IDictionaryEnumerator_Current_AfterEn
832831
object current, key, value, entry;
833832
IDictionaryEnumerator enumerator = NonGenericIDictionaryFactory(count).GetEnumerator();
834833
while (enumerator.MoveNext()) ;
835-
if (Enumerator_Current_UndefinedOperation_Throws)
834+
if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws)
836835
{
837836
Assert.Throws<InvalidOperationException>(() => enumerator.Current);
838837
Assert.Throws<InvalidOperationException>(() => enumerator.Key);

src/libraries/Common/tests/System/Collections/IEnumerable.NonGeneric.Tests.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ public abstract partial class IEnumerable_NonGeneric_Tests : TestBase
6060
/// </summary>
6161
protected virtual bool Enumerator_Current_UndefinedOperation_Throws => false;
6262

63+
/// <summary>
64+
/// When calling Current of the empty enumerator before the first MoveNext, after the end of the collection,
65+
/// or after modification of the enumeration, the resulting behavior is undefined. Tests are included
66+
/// to cover two behavioral scenarios:
67+
/// - Throwing an InvalidOperationException
68+
/// - Returning an undefined value.
69+
///
70+
/// If this property is set to true, the tests ensure that the exception is thrown. The default value is
71+
/// false.
72+
/// </summary>
73+
protected virtual bool Enumerator_Empty_Current_UndefinedOperation_Throw => Enumerator_Current_UndefinedOperation_Throws;
74+
6375
/// <summary>
6476
/// When calling MoveNext or Reset after modification of the enumeration, the resulting behavior is
6577
/// undefined. Tests are included to cover two behavioral scenarios:
@@ -305,7 +317,7 @@ public virtual void Enumerator_Current_BeforeFirstMoveNext_UndefinedBehavior(int
305317
object current;
306318
IEnumerable enumerable = NonGenericIEnumerableFactory(count);
307319
IEnumerator enumerator = enumerable.GetEnumerator();
308-
if (Enumerator_Current_UndefinedOperation_Throws)
320+
if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws)
309321
Assert.Throws<InvalidOperationException>(() => enumerator.Current);
310322
else
311323
current = enumerator.Current;
@@ -319,7 +331,7 @@ public virtual void Enumerator_Current_AfterEndOfEnumerable_UndefinedBehavior(in
319331
IEnumerable enumerable = NonGenericIEnumerableFactory(count);
320332
IEnumerator enumerator = enumerable.GetEnumerator();
321333
while (enumerator.MoveNext()) ;
322-
if (Enumerator_Current_UndefinedOperation_Throws)
334+
if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws)
323335
Assert.Throws<InvalidOperationException>(() => enumerator.Current);
324336
else
325337
current = enumerator.Current;
@@ -336,7 +348,7 @@ public virtual void Enumerator_Current_ModifiedDuringEnumeration_UndefinedBehavi
336348
IEnumerator enumerator = enumerable.GetEnumerator();
337349
if (ModifyEnumerable(enumerable))
338350
{
339-
if (Enumerator_Current_UndefinedOperation_Throws)
351+
if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws)
340352
Assert.Throws<InvalidOperationException>(() => enumerator.Current);
341353
else
342354
current = enumerator.Current;

src/libraries/Common/tests/System/Collections/IList.Generic.Tests.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,31 @@ protected override IEnumerable<ModifyEnumerable> GetModifyEnumerables(ModifyOper
103103
public void IList_Generic_ItemGet_NegativeIndex_ThrowsException(int count)
104104
{
105105
IList<T> list = GenericIListFactory(count);
106+
106107
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[-1]);
107108
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[int.MinValue]);
109+
110+
if (list is IReadOnlyList<T> rol)
111+
{
112+
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[-1]);
113+
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[int.MinValue]);
114+
}
108115
}
109116

110117
[Theory]
111118
[MemberData(nameof(ValidCollectionSizes))]
112119
public void IList_Generic_ItemGet_IndexGreaterThanListCount_ThrowsException(int count)
113120
{
114121
IList<T> list = GenericIListFactory(count);
122+
115123
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count]);
116124
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => list[count + 1]);
125+
126+
if (list is IReadOnlyList<T> rol)
127+
{
128+
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count]);
129+
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count + 1]);
130+
}
117131
}
118132

119133
[Theory]
@@ -122,7 +136,15 @@ public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count)
122136
{
123137
IList<T> list = GenericIListFactory(count);
124138
T result;
139+
125140
Assert.All(Enumerable.Range(0, count), index => result = list[index]);
141+
Assert.All(Enumerable.Range(0, count), index => Assert.Equal(list[index], list[index]));
142+
143+
if (list is IReadOnlyList<T> rol)
144+
{
145+
Assert.All(Enumerable.Range(0, count), index => result = rol[index]);
146+
Assert.All(Enumerable.Range(0, count), index => Assert.Equal(rol[index], rol[index]));
147+
}
126148
}
127149

128150
#endregion
@@ -369,7 +391,7 @@ public void IList_Generic_IndexOf_InvalidValue(int count)
369391
[MemberData(nameof(ValidCollectionSizes))]
370392
public void IList_Generic_IndexOf_ReturnsFirstMatchingValue(int count)
371393
{
372-
if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
394+
if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DuplicateValuesAllowed)
373395
{
374396
IList<T> list = GenericIListFactory(count);
375397
foreach (T duplicate in list.ToList()) // hard copies list to circumvent enumeration error

src/libraries/Common/tests/System/Collections/IList.NonGeneric.Tests.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ protected virtual object CreateT(int seed)
8383
/// </summary>
8484
protected virtual bool IList_CurrentAfterAdd_Throws => Enumerator_Current_UndefinedOperation_Throws;
8585

86+
/// <summary>
87+
/// When calling Current of the empty enumerator after the end of the list and list is extended by new items.
88+
/// Tests are included to cover two behavioral scenarios:
89+
/// - Throwing an InvalidOperationException
90+
/// - Returning an undefined value.
91+
///
92+
/// If this property is set to true, the tests ensure that the exception is thrown. The default value is
93+
/// the same as Enumerator_Current_UndefinedOperation_Throws.
94+
/// </summary>
95+
protected virtual bool IList_Empty_CurrentAfterAdd_Throws => Enumerator_Empty_Current_UndefinedOperation_Throw;
96+
8697
#endregion
8798

8899
#region ICollection Helper Methods
@@ -697,7 +708,7 @@ public void IList_NonGeneric_IndexOf_InvalidValue(int count)
697708
[MemberData(nameof(ValidCollectionSizes))]
698709
public void IList_NonGeneric_IndexOf_ReturnsFirstMatchingValue(int count)
699710
{
700-
if (!IsReadOnly && !ExpectedFixedSize)
711+
if (!IsReadOnly && !ExpectedFixedSize && DuplicateValuesAllowed)
701712
{
702713
IList list = NonGenericIListFactory(count);
703714

@@ -1084,7 +1095,7 @@ public void IList_NonGeneric_CurrentAtEnd_AfterAdd(int count)
10841095
IEnumerator enumerator = collection.GetEnumerator();
10851096
while (enumerator.MoveNext()) ; // Go to end of enumerator
10861097

1087-
if (Enumerator_Current_UndefinedOperation_Throws)
1098+
if (count == 0 ? Enumerator_Empty_Current_UndefinedOperation_Throw : Enumerator_Current_UndefinedOperation_Throws)
10881099
{
10891100
Assert.Throws<InvalidOperationException>(() => enumerator.Current); // Enumerator.Current should fail
10901101
}
@@ -1099,7 +1110,7 @@ public void IList_NonGeneric_CurrentAtEnd_AfterAdd(int count)
10991110
{
11001111
collection.Add(CreateT(seed++));
11011112

1102-
if (IList_CurrentAfterAdd_Throws)
1113+
if (count == 0 ? IList_Empty_CurrentAfterAdd_Throws : IList_CurrentAfterAdd_Throws)
11031114
{
11041115
Assert.Throws<InvalidOperationException>(() => enumerator.Current); // Enumerator.Current should fail
11051116
}

0 commit comments

Comments
 (0)