diff --git a/Recyclable.Collections/Recyclable.Collections.csproj b/Recyclable.Collections/Recyclable.Collections.csproj index b29d18c..1be10da 100644 --- a/Recyclable.Collections/Recyclable.Collections.csproj +++ b/Recyclable.Collections/Recyclable.Collections.csproj @@ -15,10 +15,17 @@ Recyclable.Collections project is an open source framework for operating dynamic lists at performance close to raw arrays. It aims at providing high performance with minimal memory footprint. It implements IList<T>'s interface and is targeted as direct replacements of List<T>, SortableList<T>, PriorityQueue<T> and similar. The project & source code is free for educational, open-source & personal use. + https://github.com/mlemanczyk/Recyclable.Collections + https://github.com/mlemanczyk/Recyclable.Collections + Marek Lemańczyk 2023 + https://github.com/mlemanczyk/Recyclable.Collections + /README.md - + + PreserveNewest + diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 76d836b..dc22b6a 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -3,8 +3,6 @@ using System.Collections; namespace Recyclable.Collections { - public delegate void ItemAdded(long index, T item); - public class RecyclableList : IDisposable, IList { private static readonly IEqualityComparer _comparer = EqualityComparer.Default; @@ -13,9 +11,20 @@ namespace Recyclable.Collections private readonly List _arrays = new(); private bool _disposedValue; private long _capacity; + protected uint _isUpdating; - private ItemAdded? _itemAdded; - public event ItemAdded ItemAdded { add => _itemAdded += value; remove => _itemAdded -= value; } + protected virtual void BlockUpdateEvent() + { + _isUpdating++; + } + + protected virtual void UnblockUpdateEvent() + { + if (_isUpdating > 0) + { + _isUpdating--; + } + } private static IEnumerable EnumerateElements(List arrays, int chunkSize, long totalCount) { @@ -58,9 +67,17 @@ namespace Recyclable.Collections public RecyclableList(IEnumerable source, int blockSize = RecyclableDefaults.BlockSize) { _blockSize = blockSize; - foreach (var item in source) + BlockUpdateEvent(); + try { - Add(item); + foreach (var item in source) + { + Add(item); + } + } + finally + { + UnblockUpdateEvent(); } } @@ -71,7 +88,10 @@ namespace Recyclable.Collections set { _arrays[(int)(index / _blockSize)][index % _blockSize] = value; - _itemAdded?.Invoke(index, value); + if (!IsUpdating) + { + ListUpdated(); + } } } @@ -84,6 +104,7 @@ namespace Recyclable.Collections public int Count => LongCount <= int.MaxValue ? (int)LongCount : int.MaxValue; public long LongCount { get; protected set; } public bool IsReadOnly { get; } = false; + public bool IsUpdating => _isUpdating > 0; public void Add(T item) { @@ -95,15 +116,28 @@ namespace Recyclable.Collections } var newIndex = LongCount; - this[newIndex] = item; - // Don't merge these 2 lines, otherwise we'll have increased count - // before the item is actually added. - LongCount++; - _itemAdded?.Invoke(newIndex, item); + // We don't want this[newIndex] set to raise the update event, because + // the count would be wrong. We need to increase it, first. + BlockUpdateEvent(); + try + { + this[newIndex] = item; + LongCount++; + } + finally + { + UnblockUpdateEvent(); + } + + if (!IsUpdating) + { + ListUpdated(); + } } public void Clear() { + BlockUpdateEvent(); try { for (var arrayIdx = 0; arrayIdx < _arrays.Count; arrayIdx++) @@ -121,9 +155,16 @@ namespace Recyclable.Collections } finally { - // Make sure the list is cleared so that we no longer use any of the arrays - LongCount = 0; - _arrays.Clear(); + try + { + // Make sure the list is cleared so that we no longer use any of the arrays + LongCount = 0; + _arrays.Clear(); + } + finally + { + UnblockUpdateEvent(); + } } } @@ -203,6 +244,10 @@ namespace Recyclable.Collections } } + protected virtual void ListUpdated() + { + } + // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources // ~RecyclableList() // { @@ -216,5 +261,19 @@ namespace Recyclable.Collections Dispose(disposing: true); GC.SuppressFinalize(this); } + + public void BeginUpdate() => _isUpdating++; + + public void EndUpdate() + { + if (_isUpdating > 0) + { + _isUpdating--; + if (_isUpdating == 0) + { + ListUpdated(); + } + } + } } } diff --git a/Recyclable.Collections/RecyclableSortedList.cs b/Recyclable.Collections/RecyclableSortedList.cs new file mode 100644 index 0000000..78f3382 --- /dev/null +++ b/Recyclable.Collections/RecyclableSortedList.cs @@ -0,0 +1,43 @@ +namespace Recyclable.Collections +{ + public class RecyclableSortedList : RecyclableList<(TKey Key, TValue Value)> + where TKey : notnull + { + private static readonly IComparer<(TKey, TValue)> _comparer = new FrequencyTupleComparer(); + private readonly SystemRandomNumberGenerator _randomNumberGenerator = new SystemRandomNumberGenerator(); + + protected override void ListUpdated() + { + base.ListUpdated(); + QuickSort(); + } + + public RecyclableSortedList() : base() + { + } + + public RecyclableSortedList(Dictionary source, int blockSize = RecyclableDefaults.BlockSize) + : this(source.Select(x => (x.Key, x.Value)), blockSize) + { + } + + public RecyclableSortedList(IEnumerable<(TKey Key, TValue Value)> source, int blockSize = RecyclableDefaults.BlockSize) + : base(source, blockSize) + { + QuickSort(); + } + + public void QuickSort() + { + _isUpdating++; + try + { + this.QuickSort(_comparer, _randomNumberGenerator); + } + finally + { + _isUpdating--; + } + } + } +} diff --git a/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj index df55634..12982dd 100644 --- a/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj +++ b/Recyclable.CollectionsTests/Recyclable.CollectionsTests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -8,10 +8,6 @@ false - - - - diff --git a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs new file mode 100644 index 0000000..0096f7f --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + [TestClass()] + public class RecyclableSortedListTests + { + private static readonly (int, string)[] _testData = new[] { (5, "a"), (4, "d"), (3, "c"), (2, "b"), (1, "a") }; + + [TestMethod] + public void ShouldBeSortedWhenCreated() + { + // Act + using var sortedList = new RecyclableSortedList(_testData); + + // Validate + _ = sortedList.Should().HaveCount(_testData.Length) + .And.BeEquivalentTo(_testData) + .And.BeInAscendingOrder((item) => item.Key); + } + + [TestMethod] + public void ShouldBeEmptyWhenNotInitialized() + { + // Act + using var sortedList = new RecyclableSortedList(); + + // Validate + _ = sortedList.Should().BeEmpty(); + } + + [TestMethod] + public void ShouldBeSortedWhenInitialized() + { + // Act + using var sortedList = new RecyclableSortedList() + { + _testData[0], + _testData[1], + _testData[2], + _testData[3], + _testData[4] + }; + + // Validate + _ = sortedList.Should().NotBeEmpty() + .And.BeInAscendingOrder(x => x.Key) + .And.BeEquivalentTo(_testData); + } + + [TestMethod] + public void ShouldBeSortedWhenUpdateEnds() + { + // Prepare + using var sortedList = new RecyclableSortedList(); + + // Act + sortedList.BeginUpdate(); + foreach (var testItem in _testData) + { + sortedList.Add(testItem); + } + + // Validate + _ = sortedList.Should().BeInDescendingOrder(x => x.Key); + + // Act + sortedList.EndUpdate(); + + // Validate + _ = sortedList.Should().BeInAscendingOrder(x => x.Key); + } + + [TestMethod] + public void ShouldBeEmptyAfterClear() + { + // Prepare + using var sortedList = new RecyclableSortedList(_testData); + + // Act + sortedList.Clear(); + + // Validate + _ = sortedList.Should().BeEmpty(); + } + } +} \ No newline at end of file