From aba28028b1875ff37c7e6ea4daf4d8deb18cc217 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Fri, 6 Jan 2023 13:54:32 +0100 Subject: [PATCH] Performance & standardization improvements. Business logic fixes. RecyclableQueue & RecyclableStack added - pending improvements. --- .../DelegateVsComparerBenchmark.cs | 253 ++++++++++++++++++ .../ListVsLinkedListBenchmark.cs | 223 +++++++++++++++ Recyclable.Collections.Benchmarks/Program.cs | 8 + .../Recyclable.Collections.Benchmarks.csproj | 20 ++ Recyclable.Collections.sln | 6 + Recyclable.Collections/IRecyclableOwner.cs | 10 + Recyclable.Collections/ListExtensions.cs | 13 +- Recyclable.Collections/RecyclableList.cs | 218 ++++----------- .../RecyclableListExtensions.cs | 125 ++++++++- Recyclable.Collections/RecyclableQueue.cs | 188 +++++++++++++ .../RecyclableSortedList.cs | 48 +++- Recyclable.Collections/RecyclableStack.cs | 86 ++++++ .../SystemRandomNumberGenerator.cs | 2 +- .../RecyclableListTests.cs | 6 +- .../RecyclableQueueTests.cs | 120 +++++++++ .../RecyclableSortedListTests.cs | 13 +- .../RecyclableStackTests.cs | 120 +++++++++ 17 files changed, 1269 insertions(+), 190 deletions(-) create mode 100644 Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs create mode 100644 Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs create mode 100644 Recyclable.Collections.Benchmarks/Program.cs create mode 100644 Recyclable.Collections.Benchmarks/Recyclable.Collections.Benchmarks.csproj create mode 100644 Recyclable.Collections/IRecyclableOwner.cs create mode 100644 Recyclable.Collections/RecyclableQueue.cs create mode 100644 Recyclable.Collections/RecyclableStack.cs create mode 100644 Recyclable.CollectionsTests/RecyclableQueueTests.cs create mode 100644 Recyclable.CollectionsTests/RecyclableStackTests.cs diff --git a/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs b/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs new file mode 100644 index 0000000..c438886 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/DelegateVsComparerBenchmark.cs @@ -0,0 +1,253 @@ +using BenchmarkDotNet.Attributes; +using MiscUtil; +using MiscUtil.Extensions; + +namespace Recyclable.Collections.Benchmarks +{ + public delegate T Compare(T x, T y); + + public readonly struct MyComparerReadOnlyStruct : IComparer + { + public int Compare(T? x, T? y) => Operator.Subtract(x, y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0, + }; + } + + public struct MyComparerStruct : IComparer + { + public int Compare(T? x, T? y) => Operator.Subtract(x, y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0, + }; + } + + public class MyComparer : IComparer + { + public int Compare(long x, long y) => (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0, + }; + } + + [MemoryDiagnoser] + public class DelegateVsComparerBenchmark + { + private static readonly Comparer _comparer = Comparer.Default; + + private const int _objectCount = 1_000_000; + private static readonly IEnumerable _testObjects = Enumerable.Range(1, _objectCount).Select(x => (long)x); + private static readonly IEnumerable> _testListOfFunctions = _testObjects.Select>(static x => new MyComparer().Compare); + private static readonly IEnumerable> _testListOfComparers = _testObjects.Select(x => (IComparer)new MyComparer()); + private static readonly IEnumerable> _testListOfDelegates = _testObjects.Select(x => new Compare((x, y) => new MyComparer().Compare(x, y))); + private static readonly IEnumerable> _testListOfLocalFunctions = GetLocalFunctions(_testObjects); + private static readonly IEnumerable _testListOfClasses = _testObjects.Select(x => new MyComparer()); + private static readonly IEnumerable> _testListOfStructs = _testObjects.Select(x => new MyComparerStruct()); + private static readonly IEnumerable> _testListOfBoxedStructs = _testObjects.Select(x => (IComparer)new MyComparerStruct()); + private static IEnumerable _testListOfUnboxedStructs() where TComparer : IComparer, new() + { + foreach (var item in _testObjects) + { + yield return new TComparer(); + } + } + + private static IEnumerable> GetLocalFunctions(IEnumerable testObjects) + { + static int compareFunc(long x, long y) + { + return (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }; + } + + foreach (var item in testObjects) + { + yield return compareFunc; + } + } + + private static void TestWithComparer(IComparer comparer) + { + var testData = _testObjects.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length - 1; itemIdx++) + { + _ = comparer.Compare(testData[itemIdx], testData[itemIdx + 1]); + } + } + + [Benchmark(Baseline = true)] + public void Comparer() + { + TestWithComparer(_comparer); + } + + private static void TestWithFunc(Func compareFunc) + { + var testData = _testObjects.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length - 1; itemIdx++) + { + _ = compareFunc(testData[itemIdx], testData[itemIdx + 1]); + } + } + + [Benchmark] + public void Func() + { + TestWithFunc((x, y) => (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }); + } + + [Benchmark] + public void StaticFunc() + { + TestWithFunc(static (x, y) => (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }); + } + + private static void TestWithDelegate(Compare compareFunc) + { + var testData = _testObjects.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length - 1; itemIdx++) + { + _ = compareFunc(testData[itemIdx], testData[itemIdx + 1]); + } + } + + [Benchmark] + public void Delegate() + { + TestWithDelegate((x, y) => (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }); + } + + [Benchmark] + public void ListOfFuncs() + { + const long x = 2, y = 5; + var testData = _testListOfFunctions.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var compareFunc = testData[itemIdx]; + _ = compareFunc(x, y); + } + } + + [Benchmark] + public void ListOfLocalFuncs() + { + const long x = 2, y = 5; + var testData = _testListOfLocalFunctions.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var compareFunc = testData[itemIdx]; + _ = compareFunc(x, y); + } + } + + [Benchmark] + public void ListOfComparers() + { + const long x = 2, y = 5; + var testData = _testListOfComparers.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfClasses() + { + const long x = 2, y = 5; + var testData = _testListOfClasses.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfStructs() + { + const long x = 2, y = 5; + var testData = _testListOfStructs.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfBoxedStructs() + { + const long x = 2, y = 5; + var testData = _testListOfBoxedStructs.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfUnboxedStructs() + { + const long x = 2, y = 5; + var testData = _testListOfUnboxedStructs, long>().ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfUnboxedReadOnlyStructs() + { + const long x = 2, y = 5; + var testData = _testListOfUnboxedStructs, long>().ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var comparer = testData[itemIdx]; + _ = comparer.Compare(x, y); + } + } + + [Benchmark] + public void ListOfDelegates() + { + const long x = 2, y = 5; + var testData = _testListOfDelegates.ToArray(); + for (var itemIdx = 0; itemIdx < testData.Length; itemIdx++) + { + var compareDelegate = testData[itemIdx]; + _ = compareDelegate(x, y); + } + } + } +} diff --git a/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs b/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs new file mode 100644 index 0000000..79d6b71 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/ListVsLinkedListBenchmark.cs @@ -0,0 +1,223 @@ +using BenchmarkDotNet.Attributes; + +namespace Recyclable.Collections.Benchmarks +{ + [MemoryDiagnoser] + public class ListVsLinkedListBenchmark + { + private const int _objectCount = 1_000_000; + private const int _blockSize = 100_000; + private static readonly object[] _testObjects = Enumerable.Range(1, _objectCount).Select(x => new object()).ToArray(); + + private static void DoNothing(T obj) + { + _ = obj; + } + + [Benchmark] + public void Array() + { + var list = new object[_testObjects.LongLength]; + for (var i = 0; i < _testObjects.Length; i++) + { + list[i] = _testObjects[i]; + } + + for (var i = 0; i < list.LongLength; i++) + { + DoNothing(list[i]); + } + } + + [Benchmark(Baseline = true)] + public void RecyclableList() + { + using var list = new RecyclableList(_blockSize); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Add(_testObjects[i]); + } + + for (var i = 0; i < list.Count; i++) + { + DoNothing(_testObjects[i]); + } + + list.Clear(); + } + + [Benchmark] + public void List() + { + var list = new List(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Add(_testObjects[i]); + } + + for (var i = 0; i < list.Count; i++) + { + DoNothing(list[i]); + } + + list.Clear(); + } + + //[Benchmark] + public void LinkedList() + { + var list = new LinkedList(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + _ = list.AddLast(_testObjects[i]); + } + + foreach (var node in list) + { + DoNothing(node); + } + + list.Clear(); + } + + [Benchmark] + public void RecyclableQueue() + { + var list = new RecyclableQueue(_blockSize); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Enqueue(_testObjects[i]); + } + + foreach (var node in list) + { + DoNothing(node); + } + + list.Clear(); + } + + //[Benchmark] + public void Queue() + { + var list = new Queue(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Enqueue(_testObjects[i]); + } + + foreach (var node in list) + { + DoNothing(node); + } + + list.Clear(); + } + + //[Benchmark] + public void PriorityQueue() + { + var list = new PriorityQueue(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Enqueue(_testObjects[i], i); + } + + while (list.Count > 0) + { + var node = list.Dequeue(); + DoNothing(node); + } + + list.Clear(); + } + + //[Benchmark] + public void RecyclableSortedList_InUpdateMode() + { + var list = new RecyclableSortedList(_blockSize); + list.BeginUpdate(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Add(i, _testObjects[i]); + } + + list.EndUpdate(); + + for (var i = 0; i < list.LongCount; i++) + { + DoNothing(_testObjects[i]); + } + + list.Clear(); + } + + //[Benchmark] + public void RecyclableSortedList() + { + var list = new RecyclableSortedList(_blockSize); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Add(i, _testObjects[i]); + } + + for (var i = 0; i < list.LongCount; i++) + { + DoNothing(_testObjects[i]); + } + + list.Clear(); + } + + //[Benchmark] + public void SortedList() + { + var list = new SortedList(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Add(i, _testObjects[i]); + } + + for (var i = 0; i < list.Count; i++) + { + DoNothing(_testObjects[i]); + } + + list.Clear(); + } + + //[Benchmark] + public void RecyclableStack() + { + var list = new RecyclableStack(_blockSize); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Push(_testObjects[i]); + } + + foreach (var node in list) + { + DoNothing(node); + } + + list.Clear(); + } + + //[Benchmark] + public void Stack() + { + var list = new Stack(); + for (var i = 0; i < _testObjects.LongLength; i++) + { + list.Push(_testObjects[i]); + } + + foreach (var node in list) + { + DoNothing(node); + } + + list.Clear(); + } + } +} diff --git a/Recyclable.Collections.Benchmarks/Program.cs b/Recyclable.Collections.Benchmarks/Program.cs new file mode 100644 index 0000000..68c401c --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Program.cs @@ -0,0 +1,8 @@ +using BenchmarkDotNet.Running; +using Recyclable.Collections.Benchmarks; + +BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); + +//var tests = new ListVsLinkedListBenchmark(); +//tests.RecyclableList(); \ No newline at end of file diff --git a/Recyclable.Collections.Benchmarks/Recyclable.Collections.Benchmarks.csproj b/Recyclable.Collections.Benchmarks/Recyclable.Collections.Benchmarks.csproj new file mode 100644 index 0000000..190fbfc --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Recyclable.Collections.Benchmarks.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + net6.0;net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Recyclable.Collections.sln b/Recyclable.Collections.sln index dd3c5b6..d746276 100644 --- a/Recyclable.Collections.sln +++ b/Recyclable.Collections.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclable.Collections", "R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Recyclable.CollectionsTests", "Recyclable.CollectionsTests\Recyclable.CollectionsTests.csproj", "{2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclable.Collections.Benchmarks", "Recyclable.Collections.Benchmarks\Recyclable.Collections.Benchmarks.csproj", "{CDC0CF78-FC11-43E3-9059-3261DA3639D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B7223FA-2D5B-40BF-9BD9-9D6B05DC18E2}.Release|Any CPU.Build.0 = Release|Any CPU + {CDC0CF78-FC11-43E3-9059-3261DA3639D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDC0CF78-FC11-43E3-9059-3261DA3639D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDC0CF78-FC11-43E3-9059-3261DA3639D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDC0CF78-FC11-43E3-9059-3261DA3639D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Recyclable.Collections/IRecyclableOwner.cs b/Recyclable.Collections/IRecyclableOwner.cs new file mode 100644 index 0000000..254c04f --- /dev/null +++ b/Recyclable.Collections/IRecyclableOwner.cs @@ -0,0 +1,10 @@ +namespace Recyclable.Collections +{ + public interface IRecyclableOwner : IList + { + T this[long index] { get; set; } + int BlockSize { get; } + long Capacity { get; set; } + long LongCount { get; set; } + } +} diff --git a/Recyclable.Collections/ListExtensions.cs b/Recyclable.Collections/ListExtensions.cs index ad01588..ab8dafa 100644 --- a/Recyclable.Collections/ListExtensions.cs +++ b/Recyclable.Collections/ListExtensions.cs @@ -81,13 +81,10 @@ namespace Recyclable.Collections while (stack.TryPop(out range)); } public static void QuickSort(this IList values, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) - { - values.QuickSort(0, values.Count - 1, comparer, randomNumberGenerator); - } + => values.QuickSort(0, values.Count - 1, comparer, randomNumberGenerator); + public static void QuickSort(this IList values) - { - values.QuickSort(Comparer.Default, new SystemRandomNumberGenerator()); - } + => values.QuickSort(Comparer.Default, new SystemRandomNumberGenerator()); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T SampleMedian(this IList values, int startIndex, int endIndex, IComparer comparer, IRandomNumberGenerator randomNumberGenerator) @@ -116,8 +113,6 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Swap(this IList values, int xIndex, int yIndex) - { - (values[yIndex], values[xIndex]) = (values[xIndex], values[yIndex]); - } + => (values[yIndex], values[xIndex]) = (values[xIndex], values[yIndex]); } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 4532216..3389241 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -1,132 +1,81 @@ -using System.Buffers; -using System.Collections; +using System.Collections; +using System.Runtime.CompilerServices; namespace Recyclable.Collections { public class RecyclableList : IDisposable, IList { - private static readonly IEqualityComparer _comparer = EqualityComparer.Default; + private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; - private readonly int _blockSize; private readonly List _arrays = new(); private bool _disposedValue; - private long _capacity; - protected uint _isUpdating; - - private static IEnumerable EnumerateElements(List arrays, int chunkSize, long totalCount) - { - long currentCount = 0; - for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) - { - var array = arrays[arrayIdx]; - currentCount += chunkSize; - switch (currentCount < totalCount) - { - case true: - for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) - { - yield return array[valueIdx]; - } - - break; - - case false: - var partialCount = (int)(totalCount % chunkSize); - int maxCount = partialCount > 0 ? partialCount : chunkSize; - for (var valueIdx = 0; valueIdx < maxCount; valueIdx++) - { - yield return array[valueIdx]; - } - - break; - } - } - } - - private static T[] RentArray(int minSize) => ArrayPool.Shared.Rent(minSize); - private static void ReturnArray(in T[] array) => ArrayPool.Shared.Return(array); public RecyclableList(int blockSize = RecyclableDefaults.BlockSize) { - _blockSize = blockSize; + BlockSize = blockSize; } public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) { - _blockSize = blockSize; - BeginUpdate(); - try + BlockSize = blockSize; + foreach (var item in source) { - foreach (var item in source) - { - Add(item); - } - } - finally - { - EndUpdate(); + Add(item); } } + public int BlockSize { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetItem(List arrays, in int blockSize, in long index, in T value) + => arrays[(int)(index / blockSize)][index % blockSize] = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetItem(List arrays, in int blockSize, in long index) + => arrays[(int)(index / blockSize)][index % blockSize]; + public T this[long index] { - get => _arrays[(int)(index / _blockSize)][index % _blockSize]; - - set - { - _arrays[(int)(index / _blockSize)][index % _blockSize] = value; - if (!IsUpdating) - { - ListUpdated(); - } - } + get => GetItem(_arrays, BlockSize, index); + set => SetItem(_arrays, BlockSize, index, value); } public T this[int index] { - get => this[(long)index]; - set => this[(long)index] = value; + get => GetItem(_arrays, BlockSize, index); + set => SetItem(_arrays, BlockSize, index, value); } - public int Count => LongCount <= int.MaxValue ? (int)LongCount : int.MaxValue; - public long LongCount { get; protected set; } + public long Capacity { get; set; } + public int Count => (int)LongCount; + public long LongCount { get; set; } public bool IsReadOnly { get; } = false; - public bool IsUpdating => _isUpdating > 0; + public int BlockCount => _arrays.Count; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { - if (LongCount == _capacity) + if (LongCount == Capacity) { - var newArray = RentArray(_blockSize); + T[] newArray = BlockSize.RentArrayFromPool(); _arrays.Add(newArray); - _capacity += _blockSize; + Capacity += BlockSize; } - var newIndex = LongCount; - // We don't want this[newIndex] set to raise the update event, because - // the count would be wrong. We need to increase it, first. - BeginUpdate(); - try - { - this[newIndex] = item; - LongCount++; - } - finally - { - EndUpdate(); - } + this[LongCount] = item; + LongCount++; } public void Clear() { - BeginUpdate(); try { - for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++) + // Remove in reversed order for performance savings + while (_arrays.Count > 0) { try { - ReturnArray(_arrays[arrayIdx]); + RemoveBlock(_arrays.Count - 1); } catch (Exception) { @@ -137,81 +86,30 @@ namespace Recyclable.Collections } finally { - try - { - // Make sure the list is cleared so that we no longer use any of the arrays - LongCount = 0; - _arrays.Clear(); - } - finally - { - EndUpdate(); - } + LongCount = 0; + _arrays.Clear(); } } - public bool Contains(T item) => _arrays.Any(x => x.Contains(item)); + public bool Contains(T item) => _arrays.Contains(item); - public void CopyTo(T[] array, int arrayIndex) - { - Span arrayMemory = array.AsSpan()[arrayIndex..]; - for (var arrayIdx = 0; arrayIdx < _arrays.Count - 1; arrayIdx++) - { - T[] partialArray = _arrays[arrayIdx]; - ReadOnlySpan sourceMemory = partialArray.AsSpan(); - sourceMemory = sourceMemory[.._blockSize]; - sourceMemory.CopyTo(arrayMemory); - arrayMemory = arrayMemory[_blockSize..]; - } + public void CopyTo(T[] array, int arrayIndex) => _arrays.CopyTo(0, BlockSize, (int)LongCount % BlockSize, array, arrayIndex); - if (_arrays.Count > 0) - { - ReadOnlySpan sourceMemory = _arrays[^1].AsSpan(); - var maxCount = Count % _blockSize; - sourceMemory = maxCount > 0 - ? sourceMemory[..maxCount] - : sourceMemory[.._blockSize]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerator GetEnumerator() => _arrays.Enumerate(BlockSize, LongCount).GetEnumerator(); - sourceMemory.CopyTo(arrayMemory); - } - } + public int IndexOf(T item) => (int)_arrays.LongIndexOf(BlockSize, item, _equalityComparer); - public IEnumerator GetEnumerator() => EnumerateElements(_arrays, _blockSize, LongCount).GetEnumerator(); + public void Insert(int index, T item) => throw new NotSupportedException(); - public int IndexOf(T item) - { - for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++) - { - var array = _arrays[arrayIdx]; - Span arrayMemory = ((T[]?)array).AsSpan(); - for (var memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) - { - if (_comparer.Equals(arrayMemory[memoryIdx], item)) - { - return (arrayIdx * _blockSize) + memoryIdx; - } - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long LongIndexOf(T item) => _arrays.LongIndexOf(BlockSize, item, _equalityComparer); - return -1; - } + public bool Remove(T item) => throw new NotSupportedException(); - public void Insert(int index, T item) - { - throw new NotSupportedException(); - } + public void RemoveAt(int index) => throw new NotSupportedException(); - public bool Remove(T item) - { - throw new NotSupportedException(); - } - - public void RemoveAt(int index) - { - throw new NotSupportedException(); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _arrays.Enumerate(BlockSize, LongCount).GetEnumerator(); protected virtual void Dispose(bool disposing) { @@ -226,10 +124,6 @@ namespace Recyclable.Collections } } - protected virtual void ListUpdated() - { - } - // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources // ~RecyclableList() // { @@ -244,17 +138,17 @@ namespace Recyclable.Collections GC.SuppressFinalize(this); } - public void BeginUpdate() => _isUpdating++; - - public void EndUpdate() + public void RemoveBlock(int index) { - if (_isUpdating > 0) + try { - _isUpdating--; - if (_isUpdating == 0) - { - ListUpdated(); - } + _arrays[index].ReturnToPool(); + _arrays.RemoveAt(index); + } + finally + { + Capacity -= BlockSize; + LongCount -= BlockSize; } } } diff --git a/Recyclable.Collections/RecyclableListExtensions.cs b/Recyclable.Collections/RecyclableListExtensions.cs index 9433010..3a4e145 100644 --- a/Recyclable.Collections/RecyclableListExtensions.cs +++ b/Recyclable.Collections/RecyclableListExtensions.cs @@ -1,8 +1,131 @@ -namespace Recyclable.Collections +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections { public static class RecyclableListExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this List arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + { + Span arrayMemory = destinationArray.AsSpan(); + arrayMemory = arrayMemory[destinationArrayIndex..]; + int startingArrayIndex = (int)(startingIndex / blockSize); + startingIndex %= blockSize; + if (arrays.Count > 0) + { + ReadOnlySpan sourceMemory = arrays[0].AsSpan(); + var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); + if (arrays.Count == 1) + { + maxToCopy = (int)Math.Min(maxToCopy, lastBlockSize); + } + + sourceMemory = sourceMemory[(int)startingIndex..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays.Count - 1 && arrayMemory.Length > 0; arrayIdx++) + { + ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); + var maxToCopy = Math.Min(blockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + if (arrays.Count > 1 && arrayMemory.Length > 0) + { + ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); + var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(this List arrays, T item) => arrays.Any(x => x.Contains(item)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable Enumerate(this List arrays, int chunkSize, long totalCount) + { + long currentCount = 0; + for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) + { + var array = arrays[arrayIdx]; + currentCount += chunkSize; + switch (currentCount < totalCount) + { + case true: + for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + + case false: + var partialCount = (int)(totalCount % chunkSize); + int maxCount = partialCount > 0 ? partialCount : chunkSize; + for (var valueIdx = 0; valueIdx < maxCount; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T LimitTo(this T value, T maxValue, IComparer comparer) + => comparer.Compare(value, maxValue) > 0 ? maxValue : value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long LongIndexOf(this List arrays, int blockSize, T item, IEqualityComparer comparer) + { + for (var arrayIdx = 0; arrayIdx < arrays.Count; arrayIdx++) + { + var array = arrays[arrayIdx]; + Span arrayMemory = array.AsSpan(); + for (int memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) + { + if (comparer.Equals(arrayMemory[memoryIdx], item)) + { + return (arrayIdx * (long)blockSize) + memoryIdx; + } + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ToArrayIndex(this long index, int blockSize) => (int)(index / blockSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ToItemIndex(this long index, int blockSize) => index % blockSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RecyclableList ToRecyclableList(this IList values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RecyclableList ToRecyclableList(this IEnumerable values, int blockSize = RecyclableDefaults.BlockSize) => new(values, blockSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] RentArrayFromPool(this int minSize) => ArrayPool.Shared.Rent(minSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnToPool(this T[] array) => ArrayPool.Shared.Return(array); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetItem(this List arrays, in int blockSize, in long index, in T value) + => arrays[(int)(index / blockSize)][index % blockSize] = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetItem(this List arrays, in int blockSize, in long index) + => arrays[(int)(index / blockSize)][index % blockSize]; + } } diff --git a/Recyclable.Collections/RecyclableQueue.cs b/Recyclable.Collections/RecyclableQueue.cs new file mode 100644 index 0000000..46b69eb --- /dev/null +++ b/Recyclable.Collections/RecyclableQueue.cs @@ -0,0 +1,188 @@ +using System.Buffers; +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections +{ + public class RecyclableQueue : IRecyclableOwner, IList, IDisposable + { + private static readonly IEqualityComparer _equalityComparer = EqualityComparer.Default; + private static readonly IComparer _comparer = Comparer.Default; + + private bool _disposedValue; + + protected List Arrays { get; } + protected long GetAbsoluteIndex(long index) => index + RemovedCount; + protected long GetRelativeIndex(long index) => index - RemovedCount; + protected long RemovedCount { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T[] RentArrayFromPool(int minSize) => ArrayPool.Shared.Rent(minSize); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnToPool(T[] array) => ArrayPool.Shared.Return(array); + + public RecyclableQueue(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) + { + BlockSize = blockSize; + Arrays = new(); + foreach (var item in source) + { + Enqueue(item); + } + } + + public RecyclableQueue(int blockSize = RecyclableDefaults.BlockSize) + { + BlockSize = blockSize; + Arrays = new(); + } + + public int BlockSize { get; protected set; } + public long Capacity { get; set; } + + public int Count + { + get => (int)LongCount.LimitTo(int.MaxValue, _comparer); + set => LongCount = value; + } + + public long LongCount { get; set; } + public bool IsReadOnly { get; } = false; + + public T this[int index] { get => this[(long)index]; set => this[(long)index] = value; } + public T this[long index] + { + get + { + long absoluteIndex = GetAbsoluteIndex(index); + int arrayIndex = absoluteIndex.ToArrayIndex(BlockSize); + long itemIndex = absoluteIndex.ToItemIndex(BlockSize); + return Arrays[arrayIndex][itemIndex]; + } + + set + { + long absoluteIndex = GetAbsoluteIndex(index); + int arrayIndex = absoluteIndex.ToArrayIndex(BlockSize); + long itemIndex = absoluteIndex.ToItemIndex(BlockSize); + Arrays[arrayIndex][itemIndex] = value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Enqueue(T item) + { + if (LongCount == Capacity) + { + T[] newArray = BlockSize.RentArrayFromPool(); + Arrays.Add(newArray); + Capacity += BlockSize; + } + + var newIndex = LongCount; + // We don't want this[newIndex] set to raise the update event, because + // the count would be wrong. We need to increase it, first. + this[newIndex] = item; + LongCount++; + } + + + public T Dequeue() + { + var toRemove = this[0]; + RemovedCount++; + LongCount--; + if (Capacity - LongCount == BlockSize) + { + RemoveBlock(0); + RemovedCount -= BlockSize; + } + + return toRemove; + } + + public void Add(T item) => Enqueue(item); + + public void Clear() + { + try + { + // Remove in reversed order for performance savings + while (Arrays.Count > 0) + { + try + { + RemoveBlock(Arrays.Count - 1); + } + catch (Exception) + { + // We want to try returning as many arrays, as possible, before + // the list is cleared. + } + } + } + finally + { + LongCount = 0; + RemovedCount = 0; + Arrays.Clear(); + } + } + + public bool Contains(T item) => Arrays.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => Arrays.CopyTo(RemovedCount, BlockSize, (int)(LongCount % BlockSize), array, arrayIndex); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable Enumerate() => Arrays.Enumerate(BlockSize, LongCount); + public IEnumerator GetEnumerator() => Enumerate().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public int IndexOf(T item) => (int)LongIndexOf(item).LimitTo(int.MaxValue, _comparer); + public long LongIndexOf(T item) => GetRelativeIndex(Arrays.LongIndexOf(BlockSize, item, _equalityComparer)); + public void Insert(int index, T item) => throw new NotSupportedException(); + public void RemoveAt(int index) => throw new NotSupportedException(); + + public bool TryDequeue(out T? item) + { + if (LongCount > 0) + { + item = Dequeue(); + return true; + } + + item = default; + return false; + } + + public bool Remove(T item) => throw new NotSupportedException(); + public void RemoveBlock(int index) + { + try + { + ReturnToPool(Arrays[index]); + Arrays.RemoveAt(index); + } + finally + { + Capacity -= BlockSize; + LongCount -= BlockSize; + } + } + + protected void Dispose(bool disposing) + { + if (!_disposedValue) + { + Clear(); + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Recyclable.Collections/RecyclableSortedList.cs b/Recyclable.Collections/RecyclableSortedList.cs index 78f3382..ee807e2 100644 --- a/Recyclable.Collections/RecyclableSortedList.cs +++ b/Recyclable.Collections/RecyclableSortedList.cs @@ -4,11 +4,11 @@ where TKey : notnull { private static readonly IComparer<(TKey, TValue)> _comparer = new FrequencyTupleComparer(); - private readonly SystemRandomNumberGenerator _randomNumberGenerator = new SystemRandomNumberGenerator(); + private uint _isUpdating; + private readonly SystemRandomNumberGenerator _randomNumberGenerator = new(); - protected override void ListUpdated() + protected void ListUpdated() { - base.ListUpdated(); QuickSort(); } @@ -27,17 +27,53 @@ QuickSort(); } + public RecyclableSortedList(int blockSize = RecyclableDefaults.BlockSize) + : base(blockSize) + { + } + + public bool IsUpdating => _isUpdating > 0; + + public void Add(TKey key, TValue value) => Add((key, value)); + + public void BeginUpdate() => _isUpdating++; + + public void Clear() + { + BeginUpdate(); + try + { + base.Clear(); + } + finally + { + EndUpdate(); + } + } + + public void EndUpdate(bool raiseListUpdated = true) + { + if (_isUpdating > 0) + { + _isUpdating--; + if (raiseListUpdated && _isUpdating == 0) + { + ListUpdated(); + } + } + } + public void QuickSort() { - _isUpdating++; + BeginUpdate(); try { this.QuickSort(_comparer, _randomNumberGenerator); } finally { - _isUpdating--; + EndUpdate(false); } } } -} +} \ No newline at end of file diff --git a/Recyclable.Collections/RecyclableStack.cs b/Recyclable.Collections/RecyclableStack.cs new file mode 100644 index 0000000..c8af95c --- /dev/null +++ b/Recyclable.Collections/RecyclableStack.cs @@ -0,0 +1,86 @@ +using System.Collections; + +namespace Recyclable.Collections +{ + public class RecyclableStack : IList, IDisposable + { + private bool _disposedValue; + private readonly int _blockSize; + + protected RecyclableList List { get; } + public int Count => List.Count; + public long LongCount + { + get => List.LongCount; + set => List.LongCount = value; + } + + public bool IsReadOnly => List.IsReadOnly; + + public T this[int index] { get => this[(long)index]; set => this[(long)index] = value; } + public T this[long index] { get => List[index]; set => List[index] = value; } + + public RecyclableStack(int blockSize = RecyclableDefaults.BlockSize) + { + _blockSize = blockSize; + List = new(blockSize); + } + + public RecyclableStack(IEnumerable list, int blockSize = RecyclableDefaults.BlockSize) + { + _blockSize = blockSize; + List = new(list, blockSize); + } + + public RecyclableStack(RecyclableList list, int blockSize = RecyclableDefaults.BlockSize) + { + _blockSize = blockSize; + List = new(list, blockSize); + } + + public void Push(T item) + { + List.Add(item); + } + + public T Pop() + { + var toRemove = List[LongCount - 1]; + LongCount--; + if (List.Capacity - LongCount == _blockSize) + { + List.RemoveBlock(List.BlockCount - 1); + } + + return toRemove; + } + + public int IndexOf(T item) => List.IndexOf(item); + public long LongIndexOf(T item) => List.LongIndexOf(item); + public void Insert(int index, T item) => List.Insert(index, item); + public void RemoveAt(int index) => List.RemoveAt(index); + public void Add(T item) => List.Add(item); + public void Clear() => List.Clear(); + public bool Contains(T item) => List.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => List.CopyTo(array, arrayIndex); + public bool Remove(T item) => List.Remove(item); + public IEnumerator GetEnumerator() => List.Reverse().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + Clear(); + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Recyclable.Collections/SystemRandomNumberGenerator.cs b/Recyclable.Collections/SystemRandomNumberGenerator.cs index c5c5358..baf7758 100644 --- a/Recyclable.Collections/SystemRandomNumberGenerator.cs +++ b/Recyclable.Collections/SystemRandomNumberGenerator.cs @@ -2,7 +2,7 @@ { public class SystemRandomNumberGenerator : IRandomNumberGenerator { - private static readonly Random _random = new(); + private readonly Random _random = new(); public int NextInt32(int startIndex, int endIndex) => _random.Next(startIndex, endIndex); } diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 1eba280..88ab0cb 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -25,7 +25,7 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Count.Should().Be(itemsCount, "we added so many items"); + _ = list.LongCount.Should().Be(itemsCount, "we added so many items"); for (var index = 0; index < itemsCount; index++) { var actual = list[index]; @@ -76,7 +76,6 @@ namespace Recyclable.CollectionsTests // Validate _ = list.LongCount.Should().Be(_totalObjectCount); - _ = list.Count.Should().Be(_totalObjectCount); _ = list.Should().Equal(expectedList); _ = expectedList.Should().AllSatisfy(expected => list.Contains(expected)); @@ -85,7 +84,6 @@ namespace Recyclable.CollectionsTests // Validate _ = list.LongCount.Should().Be(0); - _ = list.Count.Should().Be(0); } [TestMethod] @@ -95,7 +93,7 @@ namespace Recyclable.CollectionsTests using var list = NewRecyclableList; // Act - var actual = new int[list.Count]; + var actual = new int[list.LongCount]; list.CopyTo(actual, 0); _ = actual.Should().BeEquivalentTo(_testData); } diff --git a/Recyclable.CollectionsTests/RecyclableQueueTests.cs b/Recyclable.CollectionsTests/RecyclableQueueTests.cs new file mode 100644 index 0000000..29e3924 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableQueueTests.cs @@ -0,0 +1,120 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + [TestClass] + public class RecyclableQueueTests + { + private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; + + [TestMethod] + public void ShouldNotBeSortedWhenCreated() + { + // Act + using var list = new RecyclableQueue(_testData, 2); + + // Validate + _ = list.Should().HaveCount(_testData.Length) + .And.ContainInConsecutiveOrder(_testData); + } + + [TestMethod] + public void ShouldBeEmptyWhenNotInitialized() + { + // Act + using var list = new RecyclableQueue(2); + + // Validate + _ = list.Should().BeEmpty(); + } + + [TestMethod] + public void ShouldNotBeSortedWhenInitialized() + { + // Act + using var list = new RecyclableQueue(2) + { + _testData[0], + _testData[1], + _testData[2], + _testData[3], + _testData[4] + }; + + // Validate + _ = list.Should().NotBeEmpty() + .And.ContainInConsecutiveOrder(_testData) + .And.BeEquivalentTo(_testData); + } + + [TestMethod] + public void ShouldBeEmptyAfterClear() + { + // Prepare + using var list = new RecyclableQueue(_testData); + + // Act + list.Clear(); + + // Validate + _ = list.Should().BeEmpty(); + } + + [TestMethod] + public void DequeueShouldReturnItemsInTheSameOrder() + { + // Prepare + using var list = new RecyclableQueue(); + + // Act + for (var itemIdx = 0; itemIdx < _testData.Length; itemIdx++) + { + list.Enqueue(_testData[itemIdx]); + } + + var dequeuedItems = new RecyclableList(); + while (list.LongCount > 0) + { + dequeuedItems.Add(list.Dequeue()); + } + + // Validate + _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void DequeueShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() + { + // Prepare + using var list = new RecyclableQueue(_testData); + + // Act + while (list.LongCount > 0) + { + _ = list.Dequeue(); + } + + _ = list.Dequeue(); + } + + [TestMethod] + public void GetEnumeratorShouldYieldAllItemsInTheSameOrder() + { + // Prepare + using var list = new RecyclableQueue(_testData); + + // Act + var actual = new RecyclableList(); + var enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current); + } + + // Validate + _ = actual.Should().ContainInConsecutiveOrder(_testData); + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs index 0096f7f..82aecb9 100644 --- a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs @@ -12,7 +12,7 @@ namespace Recyclable.CollectionsTests public void ShouldBeSortedWhenCreated() { // Act - using var sortedList = new RecyclableSortedList(_testData); + using var sortedList = new RecyclableSortedList(_testData, 2); // Validate _ = sortedList.Should().HaveCount(_testData.Length) @@ -24,17 +24,17 @@ namespace Recyclable.CollectionsTests public void ShouldBeEmptyWhenNotInitialized() { // Act - using var sortedList = new RecyclableSortedList(); + using var sortedList = new RecyclableSortedList(2); // Validate _ = sortedList.Should().BeEmpty(); } [TestMethod] - public void ShouldBeSortedWhenInitialized() + public void ShouldNotBeSortedWhenInitialized() { // Act - using var sortedList = new RecyclableSortedList() + using var sortedList = new RecyclableSortedList(2) { _testData[0], _testData[1], @@ -45,15 +45,14 @@ namespace Recyclable.CollectionsTests // Validate _ = sortedList.Should().NotBeEmpty() - .And.BeInAscendingOrder(x => x.Key) - .And.BeEquivalentTo(_testData); + .And.ContainInConsecutiveOrder(_testData); } [TestMethod] public void ShouldBeSortedWhenUpdateEnds() { // Prepare - using var sortedList = new RecyclableSortedList(); + using var sortedList = new RecyclableSortedList(2); // Act sortedList.BeginUpdate(); diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs new file mode 100644 index 0000000..0946615 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -0,0 +1,120 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + [TestClass] + public class RecyclableStackTests + { + private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; + + [TestMethod] + public void GetEnumeratorShouldYieldAllItemsInReversedOrder() + { + // Prepare + using var list = new RecyclableStack(_testData); + + // Act + var actual = new RecyclableList(); + var enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current); + } + + // Validate + _ = actual.Should().ContainInConsecutiveOrder(_testData.Reverse()); + } + + [TestMethod] + public void ShouldNotBeSortedWhenCreated() + { + // Act + using var sortedList = new RecyclableStack(_testData, 2); + + // Validate + _ = sortedList.Should().HaveCount(_testData.Length) + .And.ContainInConsecutiveOrder(_testData); + } + + [TestMethod] + public void ShouldBeEmptyWhenNotInitialized() + { + // Act + using var sortedList = new RecyclableStack(2); + + // Validate + _ = sortedList.Should().BeEmpty(); + } + + [TestMethod] + public void ShouldNotBeSortedWhenInitialized() + { + // Act + using var sortedList = new RecyclableStack(2) + { + _testData[0], + _testData[1], + _testData[2], + _testData[3], + _testData[4] + }; + + // Validate + _ = sortedList.Should().NotBeEmpty() + .And.ContainInConsecutiveOrder(_testData) + .And.BeEquivalentTo(_testData); + } + + [TestMethod] + public void ShouldBeEmptyAfterClear() + { + // Prepare + using var sortedList = new RecyclableStack(_testData); + + // Act + sortedList.Clear(); + + // Validate + _ = sortedList.Should().BeEmpty(); + } + + [TestMethod] + public void PopShouldReturnItemsInReversedOrder() + { + // Prepare + using var list = new RecyclableStack(); + + // Act + for (var itemIdx = 0; itemIdx < _testData.Length; itemIdx++) + { + list.Push(_testData[itemIdx]); + } + + var dequeuedItems = new RecyclableList(); + while (list.LongCount > 0) + { + dequeuedItems.Add(list.Pop()); + } + + // Validate + _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData.Reverse()); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void PopShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() + { + // Prepare + using var list = new RecyclableQueue(_testData); + + // Act + while (list.LongCount > 0) + { + _ = list.Dequeue(); + } + + _ = list.Dequeue(); + } + } +}