// 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. /*============================================================================= ** ** ** Purpose: An array implementation of a generic stack. ** ** =============================================================================*/ using System; using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading; namespace Avalonia.Collections.Pooled { /// /// A simple stack of objects. Internally it is implemented as an array, /// so Push can be O(n). Pop is O(1). /// [DebuggerTypeProxy(typeof(StackDebugView<>))] [DebuggerDisplay("Count = {Count}")] [Serializable] public class PooledStack : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback { [NonSerialized] private ArrayPool _pool; [NonSerialized] private object? _syncRoot; private T[] _array; // Storage for stack elements. Do not rename (binary serialization) private int _size; // Number of items in the stack. Do not rename (binary serialization) private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization) private readonly bool _clearOnFree; private const int DefaultCapacity = 4; #region Constructors /// /// Create a stack with the default initial capacity. /// public PooledStack() : this(ClearMode.Auto, ArrayPool.Shared) { } /// /// Create a stack with the default initial capacity. /// public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } /// /// Create a stack with the default initial capacity. /// public PooledStack(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } /// /// Create a stack with the default initial capacity and a custom ArrayPool. /// public PooledStack(ClearMode clearMode, ArrayPool customPool) { _pool = customPool ?? ArrayPool.Shared; _array = Array.Empty(); _clearOnFree = ShouldClear(clearMode); } /// /// Create a stack with a specific initial capacity. The initial capacity /// must be a non-negative number. /// public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } /// /// Create a stack with a specific initial capacity. The initial capacity /// must be a non-negative number. /// public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } /// /// Create a stack with a specific initial capacity. The initial capacity /// must be a non-negative number. /// public PooledStack(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } /// /// Create a stack with a specific initial capacity. The initial capacity /// must be a non-negative number. /// public PooledStack(int capacity, ClearMode clearMode, ArrayPool customPool) { if (capacity < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } _pool = customPool ?? ArrayPool.Shared; _array = _pool.Rent(capacity); _clearOnFree = ShouldClear(clearMode); } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) { _pool = customPool ?? ArrayPool.Shared; _clearOnFree = ShouldClear(clearMode); switch (enumerable) { case null: ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); break; case ICollection collection: if (collection.Count == 0) { _array = Array.Empty(); } else { _array = _pool.Rent(collection.Count); collection.CopyTo(_array, 0); _size = collection.Count; } break; default: using (var list = new PooledList(enumerable)) { _array = _pool.Rent(list.Count); list.Span.CopyTo(_array); _size = list.Count; } break; } } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } /// /// Fills a Stack with the contents of a particular collection. The items are /// pushed onto the stack in the same order they are read by the enumerator. /// public PooledStack(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) { _pool = customPool ?? ArrayPool.Shared; _clearOnFree = ShouldClear(clearMode); _array = _pool.Rent(span.Length); span.CopyTo(_array); _size = span.Length; } #endregion /// /// The number of items in the stack. /// public int Count => _size; /// /// Returns the ClearMode behavior for the collection, denoting whether values are /// cleared from internal arrays before returning them to the pool. /// public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot { get { if (_syncRoot == null) { Interlocked.CompareExchange(ref _syncRoot, new object(), null); } return _syncRoot; } } /// /// Removes all Objects from the Stack. /// public void Clear() { if (_clearOnFree) { Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references. } _size = 0; _version++; } /// /// Compares items using the default equality comparer /// public bool Contains(T item) { // PERF: Internally Array.LastIndexOf calls // EqualityComparer.Default.LastIndexOf, which // is specialized for different types. This // boosts performance since instead of making a // virtual method call each iteration of the loop, // via EqualityComparer.Default.Equals, we // only make one virtual call to EqualityComparer.LastIndexOf. return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1; } /// /// This method removes all items which match the predicate. /// The complexity is O(n). /// public int RemoveWhere(Func match) { if (match == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); int freeIndex = 0; // the first free slot in items array // Find the first item which needs to be removed. while (freeIndex < _size && !match(_array[freeIndex])) freeIndex++; if (freeIndex >= _size) return 0; int current = freeIndex + 1; while (current < _size) { // Find the first item which needs to be kept. while (current < _size && match(_array[current])) current++; if (current < _size) { // copy item to the free slot. _array[freeIndex++] = _array[current++]; } } if (_clearOnFree) { // Clear the removed elements so that the gc can reclaim the references. Array.Clear(_array, freeIndex, _size - freeIndex); } int result = _size - freeIndex; _size = freeIndex; _version++; return result; } // Copies the stack into an array. public void CopyTo(T[] array, int arrayIndex) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if (arrayIndex < 0 || arrayIndex > array.Length) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); } if (array.Length - arrayIndex < _size) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); } Debug.Assert(array != _array); int srcIndex = 0; int dstIndex = arrayIndex + _size; while (srcIndex < _size) { array[--dstIndex] = _array[srcIndex++]; } } public void CopyTo(Span span) { if (span.Length < _size) { ThrowHelper.ThrowArgumentException_DestinationTooShort(); } int srcIndex = 0; int dstIndex = _size; while (srcIndex < _size) { span[--dstIndex] = _array[srcIndex++]; } } void ICollection.CopyTo(Array array, int arrayIndex) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if (array.Rank != 1) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); } if (array.GetLowerBound(0) != 0) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array); } if (arrayIndex < 0 || arrayIndex > array.Length) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); } if (array.Length - arrayIndex < _size) { ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); } try { Array.Copy(_array, 0, array, arrayIndex, _size); Array.Reverse(array, arrayIndex, _size); } catch (ArrayTypeMismatchException) { ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); } } /// /// Returns an IEnumerator for this PooledStack. /// /// public Enumerator GetEnumerator() => new Enumerator(this); /// IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); public void TrimExcess() { if (_size == 0) { ReturnArray(replaceWith: Array.Empty()); _version++; return; } int threshold = (int)(_array.Length * 0.9); if (_size < threshold) { var newArray = _pool.Rent(_size); if (newArray.Length < _array.Length) { Array.Copy(_array, newArray, _size); ReturnArray(replaceWith: newArray); _version++; } else { // The array from the pool wasn't any smaller than the one we already had, // (we can only control minimum size) so return it and do nothing. // If we create an exact-sized array not from the pool, we'll // get an exception when returning it to the pool. _pool.Return(newArray); } } } /// /// Returns the top object on the stack without removing it. If the stack /// is empty, Peek throws an InvalidOperationException. /// public T Peek() { int size = _size - 1; T[] array = _array; if ((uint)size >= (uint)array.Length) { ThrowForEmptyStack(); } return array[size]; } public bool TryPeek([MaybeNullWhen(false)] out T result) { int size = _size - 1; T[] array = _array; if ((uint)size >= (uint)array.Length) { result = default; return false; } result = array[size]; return true; } /// /// Pops an item from the top of the stack. If the stack is empty, Pop /// throws an InvalidOperationException. /// public T Pop() { int size = _size - 1; T[] array = _array; // if (_size == 0) is equivalent to if (size == -1), and this case // is covered with (uint)size, thus allowing bounds check elimination // https://github.com/dotnet/coreclr/pull/9773 if ((uint)size >= (uint)array.Length) { ThrowForEmptyStack(); } _version++; _size = size; T item = array[size]; if (_clearOnFree) { array[size] = default!; // Free memory quicker. } return item; } public bool TryPop([MaybeNullWhen(false)] out T result) { int size = _size - 1; T[] array = _array; if ((uint)size >= (uint)array.Length) { result = default; return false; } _version++; _size = size; result = array[size]; if (_clearOnFree) { array[size] = default!; // Free memory quicker. } return true; } /// /// Pushes an item to the top of the stack. /// public void Push(T item) { int size = _size; T[] array = _array; if ((uint)size < (uint)array.Length) { array[size] = item; _version++; _size = size + 1; } else { PushWithResize(item); } } // Non-inline from Stack.Push to improve its code quality as uncommon path [MethodImpl(MethodImplOptions.NoInlining)] private void PushWithResize(T item) { var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); Array.Copy(_array, newArray, _size); ReturnArray(replaceWith: newArray); _array[_size] = item; _version++; _size++; } /// /// Copies the Stack to an array, in the same order Pop would return the items. /// public T[] ToArray() { if (_size == 0) return Array.Empty(); T[] objArray = new T[_size]; int i = 0; while (i < _size) { objArray[i] = _array[_size - i - 1]; i++; } return objArray; } private void ThrowForEmptyStack() { Debug.Assert(_size == 0); throw new InvalidOperationException("Stack was empty."); } private void ReturnArray(T[]? replaceWith = null) { if (_array?.Length > 0) { try { _pool.Return(_array, clearArray: _clearOnFree); } catch (ArgumentException) { // oh well, the array pool didn't like our array } } if (!(replaceWith is null)) { _array = replaceWith; } } private static bool ShouldClear(ClearMode mode) { #if NETCOREAPP2_1 return mode == ClearMode.Always || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); #else return mode != ClearMode.Never; #endif } public void Dispose() { ReturnArray(replaceWith: Array.Empty()); _size = 0; _version++; } void IDeserializationCallback.OnDeserialization(object? sender) { // We can't serialize array pools, so deserialized PooledStacks will // have to use the shared pool, even if they were using a custom pool // before serialization. _pool = ArrayPool.Shared; } [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] public struct Enumerator : IEnumerator, IEnumerator { private readonly PooledStack _stack; private readonly int _version; private int _index; private T? _currentElement; internal Enumerator(PooledStack stack) { _stack = stack; _version = stack._version; _index = -2; _currentElement = default; } public void Dispose() { _index = -1; } public bool MoveNext() { bool retval; if (_version != _stack._version) throw new InvalidOperationException("Collection was modified during enumeration."); if (_index == -2) { // First call to enumerator. _index = _stack._size - 1; retval = (_index >= 0); if (retval) _currentElement = _stack._array[_index]; return retval; } if (_index == -1) { // End of enumeration. return false; } retval = (--_index >= 0); if (retval) _currentElement = _stack._array[_index]; else _currentElement = default; return retval; } public T Current { get { if (_index < 0) ThrowEnumerationNotStartedOrEnded(); return _currentElement!; } } private void ThrowEnumerationNotStartedOrEnded() { Debug.Assert(_index == -1 || _index == -2); throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended."); } object? IEnumerator.Current { get { return Current; } } void IEnumerator.Reset() { if (_version != _stack._version) throw new InvalidOperationException("Collection was modified during enumeration."); _index = -2; _currentElement = default; } } } }