diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index ea8553a974251..ceaadeb8ba669 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -324,6 +324,7 @@ public void Clear() { } public void CopyTo(int index, T[] array, int arrayIndex, int count) { } public void CopyTo(T[] array) { } public void CopyTo(T[] array, int arrayIndex) { } + public int EnsureCapacity(int capacity) { throw null; } public bool Exists(System.Predicate match) { throw null; } public T? Find(System.Predicate match) { throw null; } public System.Collections.Generic.List FindAll(System.Predicate match) { throw null; } @@ -443,6 +444,7 @@ void System.Collections.ICollection.CopyTo(System.Array array, int index) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public T[] ToArray() { throw null; } public void TrimExcess() { } + public int EnsureCapacity(int capacity) { throw null; } public bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T result) { throw null; } public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T result) { throw null; } public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable @@ -699,6 +701,7 @@ public void Push(T item) { } System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } void System.Collections.ICollection.CopyTo(System.Array array, int arrayIndex) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public int EnsureCapacity(int capacity) { throw null; } public T[] ToArray() { throw null; } public void TrimExcess() { } public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T result) { throw null; } diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/Queue.cs b/src/libraries/System.Collections/src/System/Collections/Generic/Queue.cs index 1581a8713de4a..f4676282bf487 100644 --- a/src/libraries/System.Collections/src/System/Collections/Generic/Queue.cs +++ b/src/libraries/System.Collections/src/System/Collections/Generic/Queue.cs @@ -31,9 +31,6 @@ public class Queue : IEnumerable, private int _size; // Number of elements. private int _version; - private const int MinimumGrow = 4; - private const int GrowFactor = 200; // double each time - // Creates a queue with room for capacity objects. The default initial // capacity and grow factor are used. public Queue() @@ -183,12 +180,7 @@ public void Enqueue(T item) { if (_size == _array.Length) { - int newcapacity = (int)(_array.Length * (long)GrowFactor / 100); - if (newcapacity < _array.Length + MinimumGrow) - { - newcapacity = _array.Length + MinimumGrow; - } - SetCapacity(newcapacity); + EnsureCapacityCore(_size + 1); } _array[_tail] = item; @@ -385,6 +377,53 @@ public void TrimExcess() } } + /// + /// Ensures that the capacity of this Queue is at least the specified . + /// + /// The minimum capacity to ensure. + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_array.Length < capacity) + { + EnsureCapacityCore(capacity); + } + + return _array.Length; + } + + private void EnsureCapacityCore(int capacity) + { + Debug.Assert(capacity >= 0); + + if (_array.Length < capacity) + { + // Array.MaxArrayLength is internal to S.P.CoreLib, replicate here. + const int MaxArrayLength = 0X7FEFFFFF; + const int GrowFactor = 2; + const int MinimumGrow = 4; + + int newcapacity = GrowFactor * _array.Length; + + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newcapacity > MaxArrayLength) newcapacity = MaxArrayLength; + + // Ensure minimum growth is respected. + newcapacity = Math.Max(newcapacity, _array.Length + MinimumGrow); + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding MaxArrayLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newcapacity < capacity) newcapacity = capacity; + + SetCapacity(newcapacity); + } + } + // Implements an enumerator for a Queue. The enumerator uses the // internal version number of the list to ensure that no modifications are // made to the list while an enumeration is in progress. diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/Stack.cs b/src/libraries/System.Collections/src/System/Collections/Generic/Stack.cs index 03e4282546be8..8c7889c2a6b46 100644 --- a/src/libraries/System.Collections/src/System/Collections/Generic/Stack.cs +++ b/src/libraries/System.Collections/src/System/Collections/Generic/Stack.cs @@ -282,12 +282,58 @@ public void Push(T item) [MethodImpl(MethodImplOptions.NoInlining)] private void PushWithResize(T item) { - Array.Resize(ref _array, (_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); + Debug.Assert(_size == _array.Length); + EnsureCapacityCore(_size + 1); _array[_size] = item; _version++; _size++; } + /// + /// Ensures that the capacity of this Stack is at least the specified . + /// If the current capacity of the Stack is less than specified , + /// the capacity is increased by continuously twice current capacity until it is at least the specified . + /// + /// The minimum capacity to ensure. + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_array.Length < capacity) + { + EnsureCapacityCore(capacity); + _version++; + } + + return _array.Length; + } + + private void EnsureCapacityCore(int capacity) + { + Debug.Assert(capacity >= 0); + + if (_array.Length < capacity) + { + // Array.MaxArrayLength is internal to S.P.CoreLib, replicate here. + const int MaxArrayLength = 0X7FEFFFFF; + + int newcapacity = _array.Length == 0 ? DefaultCapacity : 2 * _array.Length; + + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast. + if ((uint)newcapacity > MaxArrayLength) newcapacity = MaxArrayLength; + + // If computed capacity is still less than specified, set to the original argument. + // Capacities exceeding MaxArrayLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newcapacity < capacity) newcapacity = capacity; + + Array.Resize(ref _array, newcapacity); + } + } + // Copies the Stack to an array, in the same order Pop would return the items. public T[] ToArray() { diff --git a/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.EnsureCapacity.cs b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.EnsureCapacity.cs new file mode 100644 index 0000000000000..7eebfe146d451 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.EnsureCapacity.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +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 + { + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void EnsureCapacity_RequestingLargerCapacity_DoesInvalidateEnumeration(int count) + { + List list = GenericListFactory(count); + IEnumerator copiedListEnumerator = new List(list).GetEnumerator(); + IEnumerator enumerator = list.GetEnumerator(); + var capacity = list.Capacity; + + list.EnsureCapacity(capacity + 1); + + Assert.Throws(() => enumerator.MoveNext()); + } + + [Fact] + public void EnsureCapacity_NotInitialized_RequestedZero_ReturnsZero() + { + var list = new List(); + Assert.Equal(0, list.EnsureCapacity(0)); + Assert.Equal(0, list.Capacity); + } + + [Fact] + public void EnsureCapacity_NegativeCapacityRequested_Throws() + { + var list = new List(); + AssertExtensions.Throws("capacity", () => list.EnsureCapacity(-1)); + } + + const int MaxArraySize = 0X7FEFFFFF; + + [Theory] + [InlineData(5, MaxArraySize + 1)] + [InlineData(1, int.MaxValue)] + [SkipOnMono("mono forces no restrictions on array size.")] + public void EnsureCapacity_LargeCapacity_Throws(int count, int requestCapacity) + { + List list = GenericListFactory(count); + Assert.Throws(() => list.EnsureCapacity(requestCapacity)); + } + + [Theory] + [InlineData(5)] + public void EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCurrent_CapacityUnchanged(int currentCapacity) + { + var list = new List(currentCapacity); + + for (int requestCapacity = 0; requestCapacity <= currentCapacity; requestCapacity++) + { + Assert.Equal(currentCapacity, list.EnsureCapacity(requestCapacity)); + Assert.Equal(currentCapacity, list.Capacity); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCount_CapacityUnchanged(int count) + { + List list = GenericListFactory(count); + var currentCapacity = list.Capacity; + + for (int requestCapacity = 0; requestCapacity <= count; requestCapacity++) + { + Assert.Equal(currentCapacity, list.EnsureCapacity(requestCapacity)); + Assert.Equal(currentCapacity, list.Capacity); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + public void EnsureCapacity_CapacityIsAtLeastTheRequested(int count) + { + List list = GenericListFactory(count); + + int currentCapacity = list.Capacity; + int requestCapacity = currentCapacity + 1; + int newCapacity = list.EnsureCapacity(requestCapacity); + Assert.InRange(newCapacity, requestCapacity, int.MaxValue); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void EnsureCapacity_RequestingLargerCapacity_DoesNotImpactListContent(int count) + { + List list = GenericListFactory(count); + var copiedList = new List(list); + + list.EnsureCapacity(list.Capacity + 1); + Assert.Equal(copiedList, list); + } + } +} diff --git a/src/libraries/System.Collections/tests/Generic/Queue/Queue.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/Queue/Queue.Generic.Tests.cs index acdda99ea5896..a4b61f45be185 100644 --- a/src/libraries/System.Collections/tests/Generic/Queue/Queue.Generic.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/Queue/Queue.Generic.Tests.cs @@ -320,5 +320,97 @@ public void Queue_Generic_TryPeek_EmptyQueue_ReturnsFalse() Assert.False(new Queue().TryPeek(out result)); Assert.Equal(default(T), result); } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Queue_Generic_EnsureCapacity_RequestingLargerCapacity_DoesInvalidateEnumeration(int count) + { + Queue queue = GenericQueueFactory(count); + IEnumerator copiedEnumerator = new List(queue).GetEnumerator(); + IEnumerator enumerator = queue.GetEnumerator(); + + queue.EnsureCapacity(count + 1); + + Assert.Throws(() => enumerator.MoveNext()); + } + + [Fact] + public void Queue_Generic_EnsureCapacity_NotInitialized_RequestedZero_ReturnsZero() + { + var queue = GenericQueueFactory(); + Assert.Equal(0, queue.EnsureCapacity(0)); + } + + [Fact] + public void Queue_Generic_EnsureCapacity_NegativeCapacityRequested_Throws() + { + var queue = GenericQueueFactory(); + AssertExtensions.Throws("capacity", () => queue.EnsureCapacity(-1)); + } + + const int MaxArraySize = 0X7FEFFFFF; + + [Theory] + [InlineData(MaxArraySize + 1)] + [InlineData(int.MaxValue)] + [SkipOnMono("mono forces no restrictions on array size.")] + public void Queue_Generic_EnsureCapacity_LargeCapacityRequested_Throws(int requestedCapacity) + { + var queue = GenericQueueFactory(); + AssertExtensions.Throws(() => queue.EnsureCapacity(requestedCapacity)); + } + + [Theory] + [InlineData(5)] + public void Queue_Generic_EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCurrent_CapacityUnchanged(int currentCapacity) + { + var queue = new Queue(currentCapacity); + + for (int requestCapacity = 0; requestCapacity <= currentCapacity; requestCapacity++) + { + Assert.Equal(currentCapacity, queue.EnsureCapacity(requestCapacity)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Queue_Generic_EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCount_CapacityUnchanged(int count) + { + Queue queue = GenericQueueFactory(count); + + for (int requestCapacity = 0; requestCapacity <= count; requestCapacity++) + { + Assert.Equal(count, queue.EnsureCapacity(requestCapacity)); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + public void Queue_Generic_EnsureCapacity_CapacityIsAtLeastTheRequested(int count) + { + Queue queue = GenericQueueFactory(count); + + int requestCapacity = count + 1; + int newCapacity = queue.EnsureCapacity(requestCapacity); + Assert.InRange(newCapacity, requestCapacity, int.MaxValue); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Queue_Generic_EnsureCapacity_RequestingLargerCapacity_DoesNotImpactQueueContent(int count) + { + Queue queue = GenericQueueFactory(count); + var copiedList = new List(queue); + + queue.EnsureCapacity(count + 1); + Assert.Equal(copiedList, queue); + + for (int i = 0; i < count; i++) + { + Assert.Equal(copiedList[i], queue.Dequeue()); + } + } } } diff --git a/src/libraries/System.Collections/tests/Generic/Stack/Stack.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/Stack/Stack.Generic.Tests.cs index 21d4423b25c16..9f3f1f2dd2a7e 100644 --- a/src/libraries/System.Collections/tests/Generic/Stack/Stack.Generic.Tests.cs +++ b/src/libraries/System.Collections/tests/Generic/Stack/Stack.Generic.Tests.cs @@ -286,5 +286,97 @@ public void Stack_Generic_TryPeek_EmptyStack_ReturnsFalse() Assert.False(new Stack().TryPeek(out result)); Assert.Equal(default(T), result); } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Stack_Generic_EnsureCapacity_RequestingLargerCapacity_DoesInvalidateEnumeration(int count) + { + Stack stack = GenericStackFactory(count); + IEnumerator copiedEnumerator = new List(stack).GetEnumerator(); + IEnumerator enumerator = stack.GetEnumerator(); + + stack.EnsureCapacity(count + 1); + + Assert.Throws(() => enumerator.MoveNext()); + } + + [Fact] + public void Stack_Generic_EnsureCapacity_NotInitialized_RequestedZero_ReturnsZero() + { + var stack = GenericStackFactory(); + Assert.Equal(0, stack.EnsureCapacity(0)); + } + + [Fact] + public void Stack_Generic_EnsureCapacity_NegativeCapacityRequested_Throws() + { + var stack = GenericStackFactory(); + AssertExtensions.Throws("capacity", () => stack.EnsureCapacity(-1)); + } + + const int MaxArraySize = 0X7FEFFFFF; + + [Theory] + [InlineData(MaxArraySize + 1)] + [InlineData(int.MaxValue)] + [SkipOnMono("mono forces no restrictions on array size.")] + public void Stack_Generic_EnsureCapacity_LargeCapacityRequested_Throws(int requestedCapacity) + { + var stack = GenericStackFactory(); + AssertExtensions.Throws(() => stack.EnsureCapacity(requestedCapacity)); + } + + [Theory] + [InlineData(5)] + public void Stack_Generic_EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCurrent_CapacityUnchanged(int currentCapacity) + { + var stack = new Stack(currentCapacity); + + for (int requestCapacity = 0; requestCapacity <= currentCapacity; requestCapacity++) + { + Assert.Equal(currentCapacity, stack.EnsureCapacity(requestCapacity)); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Stack_Generic_EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCount_CapacityUnchanged(int count) + { + Stack stack = GenericStackFactory(count); + + for (int requestCapacity = 0; requestCapacity <= count; requestCapacity++) + { + Assert.Equal(count, stack.EnsureCapacity(requestCapacity)); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + public void Stack_Generic_EnsureCapacity_CapacityIsAtLeastTheRequested(int count) + { + Stack stack = GenericStackFactory(count); + + int requestCapacity = count + 1; + int newCapacity = stack.EnsureCapacity(requestCapacity); + Assert.InRange(newCapacity, requestCapacity, int.MaxValue); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void Stack_Generic_EnsureCapacity_RequestingLargerCapacity_DoesNotImpactStackContent(int count) + { + Stack stack = GenericStackFactory(count); + var copiedList = new List(stack); + + stack.EnsureCapacity(count + 1); + Assert.Equal(copiedList, stack); + + for (int i = 0; i < count; i++) + { + Assert.Equal(copiedList[i], stack.Pop()); + } + } } } diff --git a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj index 344f354855981..a9aba5d5add00 100644 --- a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj +++ b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj @@ -93,6 +93,7 @@ + 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 6da36e0f57bf4..dba078158c66c 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 @@ -214,7 +214,7 @@ public void Add(T item) private void AddWithResize(T item) { int size = _size; - EnsureCapacity(size + 1); + EnsureCapacityCore(size + 1); _size = size + 1; _items[size] = item; } @@ -391,21 +391,48 @@ public void CopyTo(T[] array, int arrayIndex) Array.Copy(_items, 0, array, arrayIndex, _size); } - // Ensures that the capacity of this list is at least the given minimum - // value. If the current capacity of the list is less than min, the - // capacity is increased to twice the current capacity or to min, - // whichever is larger. - // - private void EnsureCapacity(int min) + /// + /// Ensures that the capacity of this list is at least the specified . + /// If the current capacity of the list is less than specified , + /// the capacity is increased by continuously twice current capacity until it is at least the specified . + /// + /// The minimum capacity to ensure. + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + if (_items.Length < capacity) + { + EnsureCapacityCore(capacity); + _version++; + } + + return _items.Length; + } + + /// + /// Increase the capacity of this list to at least the specified by continuously twice current capacity. + /// + /// The minimum capacity to ensure. + private void EnsureCapacityCore(int capacity) { - if (_items.Length < min) + Debug.Assert(capacity >= 0); + + if (_items.Length < capacity) { - int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + int newcapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length; + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast - if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; - if (newCapacity < min) newCapacity = min; - Capacity = newCapacity; + if ((uint)newcapacity > Array.MaxArrayLength) newcapacity = Array.MaxArrayLength; + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding MaxArrayLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newcapacity < capacity) newcapacity = capacity; + + Capacity = newcapacity; } } @@ -668,7 +695,7 @@ public void Insert(int index, T item) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); } - if (_size == _items.Length) EnsureCapacity(_size + 1); + if (_size == _items.Length) EnsureCapacityCore(_size + 1); if (index < _size) { Array.Copy(_items, index, _items, index + 1, _size - index); @@ -714,7 +741,7 @@ public void InsertRange(int index, IEnumerable collection) int count = c.Count; if (count > 0) { - EnsureCapacity(_size + count); + EnsureCapacityCore(_size + count); if (index < _size) { Array.Copy(_items, index, _items, index + count, _size - index);