diff --git a/src/libraries/Common/tests/System/Collections/TestBase.Generic.cs b/src/libraries/Common/tests/System/Collections/TestBase.Generic.cs index 44c272dbd933f3..3769da939c0e6e 100644 --- a/src/libraries/Common/tests/System/Collections/TestBase.Generic.cs +++ b/src/libraries/Common/tests/System/Collections/TestBase.Generic.cs @@ -37,42 +37,49 @@ public abstract class TestBase : TestBase /// MemberData to be passed to tests that take an IEnumerable{T}. This method returns every permutation of /// EnumerableType to test on (e.g. HashSet, Queue), and size of set to test with (e.g. 0, 1, etc.). /// - public static IEnumerable EnumerableTestData() + public static IEnumerable EnumerableTestData() => + ((IEnumerable)Enum.GetValues(typeof(EnumerableType))).SelectMany(GetEnumerableTestData); + + /// + /// MemberData to be passed to tests that take an IEnumerable{T}. This method returns results for various + /// sizes of sets to test with (e.g. 0, 1, etc.) but only for List. + /// + public static IEnumerable ListTestData() => + GetEnumerableTestData(EnumerableType.List); + + private static IEnumerable GetEnumerableTestData(EnumerableType enumerableType) { foreach (object[] collectionSizeArray in ValidCollectionSizes()) { - foreach (EnumerableType enumerableType in Enum.GetValues(typeof(EnumerableType))) + int count = (int)collectionSizeArray[0]; + yield return new object[] { enumerableType, count, 0, 0, 0 }; // Empty Enumerable + yield return new object[] { enumerableType, count, count + 1, 0, 0 }; // Enumerable that is 1 larger + + if (count >= 1) + { + yield return new object[] { enumerableType, count, count, 0, 0 }; // Enumerable of the same size + yield return new object[] { enumerableType, count, count - 1, 0, 0 }; // Enumerable that is 1 smaller + yield return new object[] { enumerableType, count, count, 1, 0 }; // Enumerable of the same size with 1 matching element + yield return new object[] { enumerableType, count, count + 1, 1, 0 }; // Enumerable that is 1 longer with 1 matching element + yield return new object[] { enumerableType, count, count, count, 0 }; // Enumerable with all elements matching + yield return new object[] { enumerableType, count, count + 1, count, 0 }; // Enumerable with all elements matching plus one extra + } + + if (count >= 2) + { + yield return new object[] { enumerableType, count, count - 1, 1, 0 }; // Enumerable that is 1 smaller with 1 matching element + yield return new object[] { enumerableType, count, count + 2, 2, 0 }; // Enumerable that is 2 longer with 2 matching element + yield return new object[] { enumerableType, count, count - 1, count - 1, 0 }; // Enumerable with all elements matching minus one + yield return new object[] { enumerableType, count, count, 2, 0 }; // Enumerable of the same size with 2 matching element + if ((enumerableType == EnumerableType.List || enumerableType == EnumerableType.Queue)) + yield return new object[] { enumerableType, count, count, 0, 1 }; // Enumerable with 1 element duplicated + } + + if (count >= 3) { - int count = (int)collectionSizeArray[0]; - yield return new object[] { enumerableType, count, 0, 0, 0 }; // Empty Enumerable - yield return new object[] { enumerableType, count, count + 1, 0, 0 }; // Enumerable that is 1 larger - - if (count >= 1) - { - yield return new object[] { enumerableType, count, count, 0, 0 }; // Enumerable of the same size - yield return new object[] { enumerableType, count, count - 1, 0, 0 }; // Enumerable that is 1 smaller - yield return new object[] { enumerableType, count, count, 1, 0 }; // Enumerable of the same size with 1 matching element - yield return new object[] { enumerableType, count, count + 1, 1, 0 }; // Enumerable that is 1 longer with 1 matching element - yield return new object[] { enumerableType, count, count, count, 0 }; // Enumerable with all elements matching - yield return new object[] { enumerableType, count, count + 1, count, 0 }; // Enumerable with all elements matching plus one extra - } - - if (count >= 2) - { - yield return new object[] { enumerableType, count, count - 1, 1, 0 }; // Enumerable that is 1 smaller with 1 matching element - yield return new object[] { enumerableType, count, count + 2, 2, 0 }; // Enumerable that is 2 longer with 2 matching element - yield return new object[] { enumerableType, count, count - 1, count - 1, 0 }; // Enumerable with all elements matching minus one - yield return new object[] { enumerableType, count, count, 2, 0 }; // Enumerable of the same size with 2 matching element - if ((enumerableType == EnumerableType.List || enumerableType == EnumerableType.Queue)) - yield return new object[] { enumerableType, count, count, 0, 1 }; // Enumerable with 1 element duplicated - } - - if (count >= 3) - { - if ((enumerableType == EnumerableType.List || enumerableType == EnumerableType.Queue)) - yield return new object[] { enumerableType, count, count, 0, 1 }; // Enumerable with all elements duplicated - yield return new object[] { enumerableType, count, count - 1, 2, 0 }; // Enumerable that is 1 smaller with 2 matching elements - } + if ((enumerableType == EnumerableType.List || enumerableType == EnumerableType.Queue)) + yield return new object[] { enumerableType, count, count, 0, 1 }; // Enumerable with all elements duplicated + yield return new object[] { enumerableType, count, count - 1, 2, 0 }; // Enumerable that is 1 smaller with 2 matching elements } } } diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 4c5dc875d17181..755cabad1748cc 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -42,15 +42,6 @@ public static partial class StructuralComparisons } namespace System.Collections.Generic { - public static partial class CollectionExtensions - { - public static TValue? GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key) { throw null; } - public static TValue GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) { throw null; } - public static bool Remove(this System.Collections.Generic.IDictionary dictionary, TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } - public static bool TryAdd(this System.Collections.Generic.IDictionary dictionary, TKey key, TValue value) { throw null; } - public static System.Collections.ObjectModel.ReadOnlyCollection AsReadOnly(this IList list) { throw null; } - public static System.Collections.ObjectModel.ReadOnlyDictionary AsReadOnly(this IDictionary dictionary) where TKey : notnull { throw null; } - } public sealed partial class LinkedListNode { public LinkedListNode(T value) { } @@ -411,6 +402,18 @@ void System.Collections.IEnumerator.Reset() { } #endif // !BUILDING_CORELIB_REFERENCE namespace System.Collections.Generic { + public static partial class CollectionExtensions + { + public static void AddRange(this System.Collections.Generic.List list, System.ReadOnlySpan source) { } + public static void CopyTo(this System.Collections.Generic.List list, System.Span destination) { } + public static TValue? GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key) { throw null; } + public static TValue GetValueOrDefault(this System.Collections.Generic.IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) { throw null; } + public static void InsertRange(this System.Collections.Generic.List list, int index, System.ReadOnlySpan source) { } + public static bool Remove(this System.Collections.Generic.IDictionary dictionary, TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } + public static bool TryAdd(this System.Collections.Generic.IDictionary dictionary, TKey key, TValue value) { throw null; } + public static System.Collections.ObjectModel.ReadOnlyCollection AsReadOnly(this IList list) { throw null; } + public static System.Collections.ObjectModel.ReadOnlyDictionary AsReadOnly(this IDictionary dictionary) where TKey : notnull { throw null; } + } public abstract partial class Comparer : System.Collections.Generic.IComparer, System.Collections.IComparer { protected Comparer() { } diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj index 7c570c8364aadd..102ab0ee950ddf 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -7,7 +7,6 @@ - (this IReadOnlyDictionary dictionary, TKey key) - { - return dictionary.GetValueOrDefault(key, default!); - } - - public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) - { - ArgumentNullException.ThrowIfNull(dictionary); - - TValue? value; - return dictionary.TryGetValue(key, out value) ? value : defaultValue; - } - - public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) - { - ArgumentNullException.ThrowIfNull(dictionary); - - if (!dictionary.ContainsKey(key)) - { - dictionary.Add(key, value); - return true; - } - - return false; - } - - public static bool Remove(this IDictionary dictionary, TKey key, [MaybeNullWhen(false)] out TValue value) - { - ArgumentNullException.ThrowIfNull(dictionary); - - if (dictionary.TryGetValue(key, out value)) - { - dictionary.Remove(key); - return true; - } - - value = default; - return false; - } - - /// - /// Returns a read-only wrapper - /// for the specified list. - /// - /// The type of elements in the collection. - /// The list to wrap. - /// An object that acts as a read-only wrapper around the current . - /// is null. - public static ReadOnlyCollection AsReadOnly(this IList list) - { - return new ReadOnlyCollection(list); - } - - /// - /// Returns a read-only wrapper - /// for the current dictionary. - /// - /// The type of keys in the dictionary. - /// The type of values in the dictionary. - /// The dictionary to wrap. - /// An object that acts as a read-only wrapper around the current . - /// is null. - public static ReadOnlyDictionary AsReadOnly(this IDictionary dictionary) where TKey : notnull - { - return new ReadOnlyDictionary(dictionary); - } - } -} diff --git a/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.AddRange.cs b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.AddRange.cs index 0344a57c4717da..26298c68791107 100644 --- a/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.AddRange.cs +++ b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.AddRange.cs @@ -36,6 +36,35 @@ public void AddRange(EnumerableType enumerableType, int listLength, int enumerab }); } + [Theory] + [MemberData(nameof(ListTestData))] + public void AddRange_Span(EnumerableType enumerableType, int listLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) + { + List list = GenericListFactory(listLength); + List listBeforeAdd = list.ToList(); + Span span = CreateEnumerable(enumerableType, list, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements).ToArray(); + list.AddRange(span); + + // Check that the first section of the List is unchanged + Assert.All(Enumerable.Range(0, listLength), index => + { + Assert.Equal(listBeforeAdd[index], list[index]); + }); + + // Check that the added elements are correct + for (int i = 0; i < enumerableLength; i++) + { + Assert.Equal(span[i], list[i + listLength]); + }; + } + + [Fact] + public void AddRange_NullList_ThrowsArgumentNullException() + { + AssertExtensions.Throws("list", () => CollectionExtensions.AddRange(null, default)); + AssertExtensions.Throws("list", () => CollectionExtensions.AddRange(null, new int[1])); + } + [Theory] [MemberData(nameof(ValidCollectionSizes))] public void AddRange_NullEnumerable_ThrowsArgumentNullException(int count) diff --git a/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.CopyTo.cs b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.CopyTo.cs new file mode 100644 index 00000000000000..70bc155835ab1f --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.CopyTo.cs @@ -0,0 +1,45 @@ +// 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 +{ + /// + /// Contains tests that ensure the correctness of the List class. + /// + public abstract partial class List_Generic_Tests : IList_Generic_Tests + { + [Fact] + public void CopyTo_InvalidArgs_Throws() + { + AssertExtensions.Throws("list", () => CollectionExtensions.CopyTo(null, Span.Empty)); + AssertExtensions.Throws("list", () => CollectionExtensions.CopyTo(null, new Span(new int[1]))); + + var list = new List() { 1, 2, 3 }; + Assert.Throws(() => CollectionExtensions.CopyTo(list, (Span)new int[2])); + } + + [Fact] + public void CopyTo_ItemsCopiedCorrectly() + { + List list; + Span destination; + + list = new List(); + destination = Span.Empty; + list.CopyTo(destination); + + list = new List() { 1, 2, 3 }; + destination = new int[3]; + list.CopyTo(destination); + Assert.Equal(new[] { 1, 2, 3 }, destination.ToArray()); + + list = new List() { 1, 2, 3 }; + destination = new int[4]; + list.CopyTo(destination); + Assert.Equal(new[] { 1, 2, 3, 0 }, destination.ToArray()); + } + } +} diff --git a/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.InsertRange.cs b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.InsertRange.cs new file mode 100644 index 00000000000000..d72fd45dd00cc9 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.InsertRange.cs @@ -0,0 +1,51 @@ +// 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 List class. + /// + public abstract partial class List_Generic_Tests : IList_Generic_Tests + { + [Fact] + public void InsertRange_InvalidArgs_Throws() + { + AssertExtensions.Throws("list", () => CollectionExtensions.InsertRange(null, 0, ReadOnlySpan.Empty)); + AssertExtensions.Throws("list", () => CollectionExtensions.InsertRange(null, 0, new ReadOnlySpan(new int[1]))); + + var list = new List(); + AssertExtensions.Throws("index", () => CollectionExtensions.InsertRange(list, 1, new int[0])); + AssertExtensions.Throws("index", () => CollectionExtensions.InsertRange(list, -1, new int[0])); + } + + [Fact] + public void InsertRange_MatchesExpectedContents() + { + var list = new List(); + + list.InsertRange(0, ReadOnlySpan.Empty); + Assert.Equal(0, list.Count); + + list.InsertRange(0, (ReadOnlySpan)new int[] { 3, 2, 1 }); + Assert.Equal(3, list.Count); + Assert.Equal(new[] { 3, 2, 1 }, list); + + list.InsertRange(0, (ReadOnlySpan)new int[] { 6, 5, 4 }); + Assert.Equal(6, list.Count); + Assert.Equal(new[] { 6, 5, 4, 3, 2, 1 }, list); + + list.InsertRange(6, (ReadOnlySpan)new int[] { 0, -1, -2 }); + Assert.Equal(9, list.Count); + Assert.Equal(new[] { 6, 5, 4, 3, 2, 1, 0, -1, -2 }, list); + + list.InsertRange(3, (ReadOnlySpan)new int[] { 100, 99, 98 }); + Assert.Equal(12, list.Count); + Assert.Equal(new[] { 6, 5, 4, 100, 99, 98, 3, 2, 1, 0, -1, -2 }, list); + } + } +} diff --git a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj index 3855b43bfa6d9a..2187dff4f7c759 100644 --- a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj +++ b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent) true @@ -98,8 +98,10 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index d8620637574c08..e4f97a37356299 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -187,6 +187,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs new file mode 100644 index 00000000000000..068536049a65f9 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/CollectionExtensions.cs @@ -0,0 +1,164 @@ +// 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.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.Generic +{ + public static class CollectionExtensions + { + public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) => + dictionary.GetValueOrDefault(key, default!); + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) + { + if (dictionary is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); + } + + return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue; + } + + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); + } + + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, value); + return true; + } + + return false; + } + + public static bool Remove(this IDictionary dictionary, TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (dictionary is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); + } + + if (dictionary.TryGetValue(key, out value)) + { + dictionary.Remove(key); + return true; + } + + value = default; + return false; + } + + /// + /// Returns a read-only wrapper + /// for the specified list. + /// + /// The type of elements in the collection. + /// The list to wrap. + /// An object that acts as a read-only wrapper around the current . + /// is null. + public static ReadOnlyCollection AsReadOnly(this IList list) => + new ReadOnlyCollection(list); + + /// + /// Returns a read-only wrapper + /// for the current dictionary. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// The dictionary to wrap. + /// An object that acts as a read-only wrapper around the current . + /// is null. + public static ReadOnlyDictionary AsReadOnly(this IDictionary dictionary) where TKey : notnull => + new ReadOnlyDictionary(dictionary); + + /// Adds the elements of the specified span to the end of the . + /// The type of elements in the list. + /// The list to which the elements should be added. + /// The span whose elements should be added to the end of the . + /// The is null. + public static void AddRange(this List list, ReadOnlySpan source) + { + if (list is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list); + } + + if (!source.IsEmpty) + { + if (list._items.Length - list._size < source.Length) + { + list.Grow(checked(list._size + source.Length)); + } + + source.CopyTo(list._items.AsSpan(list._size)); + list._size += source.Length; + list._version++; + } + } + + /// Inserts the elements of a span into the at the specified index. + /// The type of elements in the list. + /// The list into which the elements should be inserted. + /// The zero-based index at which the new elements should be inserted. + /// The span whose elements should be added to the . + /// The is null. + /// is less than 0 or greater than 's . + public static void InsertRange(this List list, int index, ReadOnlySpan source) + { + if (list is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list); + } + + if ((uint)index > (uint)list._size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexMustBeLessOrEqualException(); + } + + if (!source.IsEmpty) + { + if (list._items.Length - list._size < source.Length) + { + list.Grow(checked(list._size + source.Length)); + } + + // If the index at which to insert is less than the number of items in the list, + // shift all items past that location in the list down to the end, making room + // to copy in the new data. + if (index < list._size) + { + Array.Copy(list._items, index, list._items, index + source.Length, list._size - index); + } + + // Copy the source span into the list. + // Note that this does not handle the unsafe case of trying to insert a CollectionsMarshal.AsSpan(list) + // or some slice thereof back into the list itself; such an operation has undefined behavior. + source.CopyTo(list._items.AsSpan(index)); + list._size += source.Length; + list._version++; + } + } + + /// Copies the entire to a span. + /// The type of elements in the list. + /// The list from which the elements are copied. + /// The span that is the destination of the elements copied from . + /// The is null. + /// The number of elements in the source is greater than the number of elements that the destination span can contain. + public static void CopyTo(this List list, Span destination) + { + if (list is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list); + } + + new ReadOnlySpan(list._items, 0, list._size).CopyTo(destination); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs index 8db8b83861ae72..6f1340e318fb5a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs @@ -24,7 +24,7 @@ public class List : IList, IList, IReadOnlyList internal T[] _items; // Do not rename (binary serialization) internal int _size; // Do not rename (binary serialization) - private int _version; // Do not rename (binary serialization) + internal int _version; // Do not rename (binary serialization) #pragma warning disable CA1825 // avoid the extra generic instantiation for Array.Empty() private static readonly T[] s_emptyArray = new T[0]; @@ -449,7 +449,7 @@ public int EnsureCapacity(int capacity) /// Increase the capacity of this list to at least the specified . /// /// The minimum capacity to ensure. - private void Grow(int capacity) + internal void Grow(int capacity) { Debug.Assert(_items.Length < capacity);