Skip to content

Commit

Permalink
Add OrderedDictionary (#103309)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephentoub committed Jun 14, 2024
1 parent 48c8805 commit 2b0e517
Show file tree
Hide file tree
Showing 18 changed files with 3,628 additions and 191 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ namespace System.Collections.Generic
/// </summary>
internal static partial class EnumerableHelpers
{
/// <summary>Calls Reset on an enumerator instance.</summary>
/// <remarks>Enables Reset to be called without boxing on a struct enumerator that lacks a public Reset.</remarks>
internal static void Reset<T>(ref T enumerator) where T : IEnumerator => enumerator.Reset();

/// <summary>Gets an enumerator singleton for an empty collection.</summary>
internal static IEnumerator<T> GetEmptyEnumerator<T>() =>
((IEnumerable<T>)Array.Empty<T>()).GetEnumerator();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,6 @@ public virtual void IDictionary_NonGeneric_Values_Enumeration_ParentDictionaryMo
{
Assert.Throws<InvalidOperationException>(() => valuesEnum.MoveNext());
Assert.Throws<InvalidOperationException>(() => valuesEnum.Reset());
Assert.Throws<InvalidOperationException>(() => valuesEnum.Current);
}
else
{
Expand Down Expand Up @@ -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<InvalidOperationException>(() => enumerator.Current);
Assert.Throws<InvalidOperationException>(() => enumerator.Key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public abstract partial class IEnumerable_NonGeneric_Tests : TestBase
/// </summary>
protected virtual bool Enumerator_Current_UndefinedOperation_Throws => false;

/// <summary>
/// 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.
/// </summary>
protected virtual bool Enumerator_Empty_Current_UndefinedOperation_Throw => Enumerator_Current_UndefinedOperation_Throws;

/// <summary>
/// When calling MoveNext or Reset after modification of the enumeration, the resulting behavior is
/// undefined. Tests are included to cover two behavioral scenarios:
Expand Down Expand Up @@ -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<InvalidOperationException>(() => enumerator.Current);
else
current = enumerator.Current;
Expand All @@ -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<InvalidOperationException>(() => enumerator.Current);
else
current = enumerator.Current;
Expand All @@ -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<InvalidOperationException>(() => enumerator.Current);
else
current = enumerator.Current;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,31 @@ protected override IEnumerable<ModifyEnumerable> GetModifyEnumerables(ModifyOper
public void IList_Generic_ItemGet_NegativeIndex_ThrowsException(int count)
{
IList<T> 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<T> rol)
{
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[-1]);
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[int.MinValue]);
}
}

[Theory]
[MemberData(nameof(ValidCollectionSizes))]
public void IList_Generic_ItemGet_IndexGreaterThanListCount_ThrowsException(int count)
{
IList<T> 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<T> rol)
{
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count]);
Assert.Throws(IList_Generic_Item_InvalidIndex_ThrowType, () => rol[count + 1]);
}
}

[Theory]
Expand All @@ -122,7 +136,15 @@ public void IList_Generic_ItemGet_ValidGetWithinListBounds(int count)
{
IList<T> 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<T> 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
Expand Down Expand Up @@ -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<T> list = GenericIListFactory(count);
foreach (T duplicate in list.ToList()) // hard copies list to circumvent enumeration error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ protected virtual object CreateT(int seed)
/// </summary>
protected virtual bool IList_CurrentAfterAdd_Throws => Enumerator_Current_UndefinedOperation_Throws;

/// <summary>
/// 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.
/// </summary>
protected virtual bool IList_Empty_CurrentAfterAdd_Throws => Enumerator_Empty_Current_UndefinedOperation_Throw;

#endregion

#region ICollection Helper Methods
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<InvalidOperationException>(() => enumerator.Current); // Enumerator.Current should fail
}
Expand All @@ -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<InvalidOperationException>(() => enumerator.Current); // Enumerator.Current should fail
}
Expand Down
Loading

0 comments on commit 2b0e517

Please sign in to comment.