Skip to content

Commit

Permalink
issue-44801 EnsureCapacity Apis For List Stack Queue (#47149)
Browse files Browse the repository at this point in the history
* issue-44801 Initial commit: public List.EnsureCapacity.

* Create Stack.EnsureCapacity().

* issue-44801 Fix comment for Stack.

* issue-44801 Fix comment: Move temp MaxArrayLength into near usage scope for Stack.

* issue-44801 Fix comment for List.

* issue-44801 Create EnsureCapacity for Queue.

* issue-44801 Create tests for List.EnsureCapacity.

* issue-44801 Create tests for Stack.EnsureCapacity.

* issue-44801 Create tests for Queue.EnsureCapacity.

* issue-44801 Update version if updating internal buffer.

* issue-44801 Update test cases to verify invalidating enumeration (List, Stack, Queue).

* issue-44801 Improve code change for List:

1. Avoid incrementing version number twice in one Insert (or Add) method call;
2. Avoid more capacity check for Insert (or Add) method.

* issue-44801 Fix comments: consider integer overflow; refactor methods.

* Update src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs

Fix comment: update xml doc.

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>

* ensure same algorithm is used for all resize operations

* revert array length check in List.EnsureCapacityCore

* remove GrowFactor constant

* skip large capacity tests on mono.

* Fix overflow handling when capacity < MaxArrayLength; add clarifying comments

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
  • Loading branch information
lateapexearlyspeed and eiriktsarpalis authored Feb 23, 2021
1 parent 0db5b46 commit 28be257
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/libraries/System.Collections/ref/System.Collections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> match) { throw null; }
public T? Find(System.Predicate<T> match) { throw null; }
public System.Collections.Generic.List<T> FindAll(System.Predicate<T> match) { throw null; }
Expand Down Expand Up @@ -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<T>, System.Collections.IEnumerator, System.IDisposable
Expand Down Expand Up @@ -699,6 +701,7 @@ public void Push(T item) { }
System.Collections.Generic.IEnumerator<T> System.Collections.Generic.IEnumerable<T>.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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ public class Queue<T> : IEnumerable<T>,
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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -385,6 +377,53 @@ public void TrimExcess()
}
}

/// <summary>
/// Ensures that the capacity of this Queue is at least the specified <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}

/// <summary>
/// Ensures that the capacity of this Stack is at least the specified <paramref name="capacity"/>.
/// If the current capacity of the Stack is less than specified <paramref name="capacity"/>,
/// the capacity is increased by continuously twice current capacity until it is at least the specified <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Contains tests that ensure the correctness of the List class.
/// </summary>
public abstract partial class List_Generic_Tests<T> : IList_Generic_Tests<T>
{
[Theory]
[MemberData(nameof(ValidCollectionSizes))]
public void EnsureCapacity_RequestingLargerCapacity_DoesInvalidateEnumeration(int count)
{
List<T> list = GenericListFactory(count);
IEnumerator<T> copiedListEnumerator = new List<T>(list).GetEnumerator();
IEnumerator<T> enumerator = list.GetEnumerator();
var capacity = list.Capacity;

list.EnsureCapacity(capacity + 1);

Assert.Throws<InvalidOperationException>(() => enumerator.MoveNext());
}

[Fact]
public void EnsureCapacity_NotInitialized_RequestedZero_ReturnsZero()
{
var list = new List<T>();
Assert.Equal(0, list.EnsureCapacity(0));
Assert.Equal(0, list.Capacity);
}

[Fact]
public void EnsureCapacity_NegativeCapacityRequested_Throws()
{
var list = new List<T>();
AssertExtensions.Throws<ArgumentOutOfRangeException>("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<T> list = GenericListFactory(count);
Assert.Throws<OutOfMemoryException>(() => list.EnsureCapacity(requestCapacity));
}

[Theory]
[InlineData(5)]
public void EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCurrent_CapacityUnchanged(int currentCapacity)
{
var list = new List<T>(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<T> 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<T> 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<T> list = GenericListFactory(count);
var copiedList = new List<T>(list);

list.EnsureCapacity(list.Capacity + 1);
Assert.Equal(copiedList, list);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -320,5 +320,97 @@ public void Queue_Generic_TryPeek_EmptyQueue_ReturnsFalse()
Assert.False(new Queue<T>().TryPeek(out result));
Assert.Equal(default(T), result);
}

[Theory]
[MemberData(nameof(ValidCollectionSizes))]
public void Queue_Generic_EnsureCapacity_RequestingLargerCapacity_DoesInvalidateEnumeration(int count)
{
Queue<T> queue = GenericQueueFactory(count);
IEnumerator<T> copiedEnumerator = new List<T>(queue).GetEnumerator();
IEnumerator<T> enumerator = queue.GetEnumerator();

queue.EnsureCapacity(count + 1);

Assert.Throws<InvalidOperationException>(() => 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<ArgumentOutOfRangeException>("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<OutOfMemoryException>(() => queue.EnsureCapacity(requestedCapacity));
}

[Theory]
[InlineData(5)]
public void Queue_Generic_EnsureCapacity_RequestedCapacitySmallerThanOrEqualToCurrent_CapacityUnchanged(int currentCapacity)
{
var queue = new Queue<T>(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<T> 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<T> 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<T> queue = GenericQueueFactory(count);
var copiedList = new List<T>(queue);

queue.EnsureCapacity(count + 1);
Assert.Equal(copiedList, queue);

for (int i = 0; i < count; i++)
{
Assert.Equal(copiedList[i], queue.Dequeue());
}
}
}
}
Loading

0 comments on commit 28be257

Please sign in to comment.