From d7d621ab0dc0107816649b7b2f4068ffd6f40e2d Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 18 Aug 2021 19:27:47 +0900 Subject: [PATCH] HashSet --- .../Internal/CopyedCollection.cs | 5 +- .../Internal/GroupedView.cs | 3 +- .../Internal/ResizableArray.cs | 54 ++++++ .../ObservableHashSet.Views.cs | 158 +++++++++++++++++- .../ObservableHashSet.cs | 140 +++++++++++++++- .../ObservableList.Views.cs | 6 +- src/ObservableCollections/ObservableQueue.cs | 3 +- src/ObservableCollections/ObservableStack.cs | 3 +- .../ObservableHashSetTest.cs | 80 +++++++++ 9 files changed, 436 insertions(+), 16 deletions(-) create mode 100644 src/ObservableCollections/Internal/ResizableArray.cs create mode 100644 tests/ObservableCollections.Tests/ObservableHashSetTest.cs diff --git a/src/ObservableCollections/Internal/CopyedCollection.cs b/src/ObservableCollections/Internal/CopyedCollection.cs index cea4c00..ff4f2c0 100644 --- a/src/ObservableCollections/Internal/CopyedCollection.cs +++ b/src/ObservableCollections/Internal/CopyedCollection.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace ObservableCollections.Internal { @@ -69,7 +70,7 @@ namespace ObservableCollections.Internal { if (array.Length == index) { - ArrayPool.Shared.Return(array); + ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences()); } array = ArrayPool.Shared.Rent(index * 2); } @@ -78,7 +79,7 @@ namespace ObservableCollections.Internal { if (array != null) { - ArrayPool.Shared.Return(array); + ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences()); array = null; } } diff --git a/src/ObservableCollections/Internal/GroupedView.cs b/src/ObservableCollections/Internal/GroupedView.cs index 7023615..6b3d065 100644 --- a/src/ObservableCollections/Internal/GroupedView.cs +++ b/src/ObservableCollections/Internal/GroupedView.cs @@ -346,7 +346,8 @@ namespace ObservableCollections.Internal var value = e.OldItem; var key = keySelector(value); - lookup + // TODO:... + // lookup //var removeItems = lookup[key]; //foreach (var v in removeItems) diff --git a/src/ObservableCollections/Internal/ResizableArray.cs b/src/ObservableCollections/Internal/ResizableArray.cs new file mode 100644 index 0000000..c520fab --- /dev/null +++ b/src/ObservableCollections/Internal/ResizableArray.cs @@ -0,0 +1,54 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace ObservableCollections.Internal +{ + internal ref struct ResizableArray + { + T[]? array; + int count; + + public ReadOnlySpan Span => array.AsSpan(0, count); + + public ResizableArray(int initialCapacity) + { + array = ArrayPool.Shared.Rent(initialCapacity); + count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + if (array == null) Throw(); + if (array.Length == count) + { + EnsureCapacity(); + } + array[count++] = item; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + void EnsureCapacity() + { + var newArray = array.AsSpan().ToArray(); + ArrayPool.Shared.Return(array!, RuntimeHelpers.IsReferenceOrContainsReferences()); + array = newArray; + } + + public void Dispose() + { + if (array != null) + { + ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences()); + array = null; + } + } + + [DoesNotReturn] + void Throw() + { + throw new ObjectDisposedException("ResizableArray"); + } + } +} diff --git a/src/ObservableCollections/ObservableHashSet.Views.cs b/src/ObservableCollections/ObservableHashSet.Views.cs index 6d3c566..d362841 100644 --- a/src/ObservableCollections/ObservableHashSet.Views.cs +++ b/src/ObservableCollections/ObservableHashSet.Views.cs @@ -1,14 +1,162 @@ -using System.Collections.Generic; +using ObservableCollections.Internal; +using System.Collections; +using System.Collections.Specialized; namespace ObservableCollections { public sealed partial class ObservableHashSet : IReadOnlyCollection, IObservableCollection { - // TODO: - - public ISynchronizedView CreateView(Func transform, bool reverse = false) + public ISynchronizedView CreateView(Func transform, bool _ = false) { - throw new NotImplementedException(); + return new View(this, transform); + } + + sealed class View : ISynchronizedView + { + readonly ObservableHashSet source; + readonly Func selector; + readonly Dictionary dict; + + ISynchronizedViewFilter filter; + + public event NotifyCollectionChangedEventHandler? RoutingCollectionChanged; + public event Action? CollectionStateChanged; + + public object SyncRoot { get; } + + public View(ObservableHashSet source, Func selector) + { + this.source = source; + this.selector = selector; + this.filter = SynchronizedViewFilter.Null; + this.SyncRoot = new object(); + lock (source.SyncRoot) + { + this.dict = source.set.ToDictionary(x => x, x => (x, selector(x))); + this.source.CollectionChanged += SourceCollectionChanged; + } + } + + public int Count + { + get + { + lock (SyncRoot) + { + return dict.Count; + } + } + } + + public void AttachFilter(ISynchronizedViewFilter filter) + { + lock (SyncRoot) + { + this.filter = filter; + foreach (var (_, (value, view)) in dict) + { + filter.InvokeOnAttach(value, view); + } + } + } + + public void ResetFilter(Action? resetAction) + { + lock (SyncRoot) + { + this.filter = SynchronizedViewFilter.Null; + if (resetAction != null) + { + foreach (var (_, (value, view)) in dict) + { + resetAction(value, view); + } + } + } + } + + public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + { + lock (SyncRoot) + { + return new NotifyCollectionChangedSynchronizedView(this); + } + } + + public IEnumerator<(T, TView)> GetEnumerator() + { + return new SynchronizedViewEnumerator(SyncRoot, dict.Select(x => x.Value).GetEnumerator(), filter); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Dispose() + { + this.source.CollectionChanged -= SourceCollectionChanged; + } + + private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs e) + { + lock (SyncRoot) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.IsSingleItem) + { + var v = (e.NewItem, selector(e.NewItem)); + dict.Add(e.NewItem, v); + filter.InvokeOnAdd(v); + } + else + { + foreach (var item in e.NewItems) + { + var v = (item, selector(item)); + dict.Add(item, v); + filter.InvokeOnAdd(v); + } + } + break; + case NotifyCollectionChangedAction.Remove: + if (e.IsSingleItem) + { + if (dict.Remove(e.OldItem, out var value)) + { + filter.InvokeOnRemove(value.Item1, value.Item2); + } + } + else + { + foreach (var item in e.OldItems) + { + if (dict.Remove(item, out var value)) + { + filter.InvokeOnRemove(value.Item1, value.Item2); + } + } + } + break; + case NotifyCollectionChangedAction.Reset: + if (!filter.IsNullFilter()) + { + foreach (var item in dict) + { + filter.InvokeOnRemove(item.Value); + } + } + dict.Clear(); + break; + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + default: + break; + } + + RoutingCollectionChanged?.Invoke(e); + CollectionStateChanged?.Invoke(e.Action); + } + } } } } diff --git a/src/ObservableCollections/ObservableHashSet.cs b/src/ObservableCollections/ObservableHashSet.cs index ce35f4c..736c6be 100644 --- a/src/ObservableCollections/ObservableHashSet.cs +++ b/src/ObservableCollections/ObservableHashSet.cs @@ -1,11 +1,12 @@ using ObservableCollections.Internal; using System.Collections; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace ObservableCollections { // can not implements ISet because set operation can not get added/removed values. public sealed partial class ObservableHashSet : IReadOnlySet, IReadOnlyCollection, IObservableCollection + where T : notnull { readonly HashSet set; public object SyncRoot { get; } = new object(); @@ -40,8 +41,143 @@ namespace ObservableCollections public bool IsReadOnly => false; - // TODO: Add, Remove, Set operations. + public bool Add(T item) + { + lock (SyncRoot) + { + if (set.Add(item)) + { + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(item, -1)); + return true; + } + return false; + } + } + + public void AddRange(IEnumerable items) + { + lock (SyncRoot) + { + if (!items.TryGetNonEnumeratedCount(out var capacity)) + { + capacity = 4; + } + + using (var list = new ResizableArray(capacity)) + { + foreach (var item in items) + { + if (set.Add(item)) + { + list.Add(item); + } + } + + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(list.Span, -1)); + } + } + } + + public void AddRange(T[] items) + { + AddRange(items.AsSpan()); + } + + public void AddRange(ReadOnlySpan items) + { + lock (SyncRoot) + { + using (var list = new ResizableArray(items.Length)) + { + foreach (var item in items) + { + if (set.Add(item)) + { + list.Add(item); + } + } + + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Add(list.Span, -1)); + } + } + } + + public bool Remove(T item) + { + lock (SyncRoot) + { + if (set.Remove(item)) + { + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Remove(item, -1)); + return true; + } + + return false; + } + } + + public void RemoveRange(IEnumerable items) + { + lock (SyncRoot) + { + if (!items.TryGetNonEnumeratedCount(out var capacity)) + { + capacity = 4; + } + + using (var list = new ResizableArray(capacity)) + { + foreach (var item in items) + { + if (set.Remove(item)) + { + list.Add(item); + } + } + + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Remove(list.Span, -1)); + } + } + } + + public void RemoveRange(T[] items) + { + RemoveRange(items.AsSpan()); + } + + public void RemoveRange(ReadOnlySpan items) + { + lock (SyncRoot) + { + using (var list = new ResizableArray(items.Length)) + { + foreach (var item in items) + { + if (set.Remove(item)) + { + list.Add(item); + } + } + + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Remove(list.Span, -1)); + } + } + } + + public void Clear() + { + lock (SyncRoot) + { + set.Clear(); + CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs.Reset()); + } + } + + public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue) + { + return set.TryGetValue(equalValue, out actualValue); + } public bool Contains(T item) { diff --git a/src/ObservableCollections/ObservableList.Views.cs b/src/ObservableCollections/ObservableList.Views.cs index 24d1124..49e463c 100644 --- a/src/ObservableCollections/ObservableList.Views.cs +++ b/src/ObservableCollections/ObservableList.Views.cs @@ -11,12 +11,12 @@ namespace ObservableCollections return new View(this, transform, reverse); } - class View : ISynchronizedView + sealed class View : ISynchronizedView { readonly ObservableList source; readonly Func selector; readonly bool reverse; - protected readonly List<(T, TView)> list; + readonly List<(T, TView)> list; ISynchronizedViewFilter filter; @@ -111,8 +111,6 @@ namespace ObservableCollections switch (e.Action) { case NotifyCollectionChangedAction.Add: - list.EnsureCapacity(e.NewItems.Length); - // Add if (e.NewStartingIndex == list.Count) { diff --git a/src/ObservableCollections/ObservableQueue.cs b/src/ObservableCollections/ObservableQueue.cs index a7c1234..e43f90b 100644 --- a/src/ObservableCollections/ObservableQueue.cs +++ b/src/ObservableCollections/ObservableQueue.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace ObservableCollections { @@ -129,7 +130,7 @@ namespace ObservableCollections } finally { - ArrayPool.Shared.Return(dest); + ArrayPool.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences()); } } } diff --git a/src/ObservableCollections/ObservableStack.cs b/src/ObservableCollections/ObservableStack.cs index 6eec575..354105e 100644 --- a/src/ObservableCollections/ObservableStack.cs +++ b/src/ObservableCollections/ObservableStack.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace ObservableCollections { @@ -126,7 +127,7 @@ namespace ObservableCollections } finally { - ArrayPool.Shared.Return(dest); + ArrayPool.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences()); } } } diff --git a/tests/ObservableCollections.Tests/ObservableHashSetTest.cs b/tests/ObservableCollections.Tests/ObservableHashSetTest.cs new file mode 100644 index 0000000..0b256a8 --- /dev/null +++ b/tests/ObservableCollections.Tests/ObservableHashSetTest.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ObservableCollections.Tests +{ + public class ObservableHashSetTest + { + [Fact] + public void View() + { + var set = new ObservableHashSet(); + var view = set.CreateView(x => new ViewContainer(x)); + + set.Add(10); + set.Add(50); + set.Add(30); + set.Add(20); + set.Add(40); + + void Equal(params int[] expected) + { + set.Should().BeEquivalentTo(expected); + view.Select(x => x.Value).Should().BeEquivalentTo(expected); + view.Select(x => x.View.Value).Should().BeEquivalentTo(expected); + } + + Equal(10, 50, 30, 20, 40); + + set.AddRange(new[] { 1, 2, 3, 4, 5 }); + Equal(10, 50, 30, 20, 40, 1, 2, 3, 4, 5); + + set.Remove(10); + Equal(50, 30, 20, 40, 1, 2, 3, 4, 5); + + set.RemoveRange(new[] { 50, 40 }); + Equal(30, 20, 1, 2, 3, 4, 5); + + set.Clear(); + + Equal(); + } + + [Fact] + public void Filter() + { + var set = new ObservableHashSet(); + var view = set.CreateView(x => new ViewContainer(x)); + var filter = new TestFilter((x, v) => x % 3 == 0); + + set.Add(10); + set.Add(50); + set.Add(30); + set.Add(20); + set.Add(40); + + view.AttachFilter(filter); + filter.CalledWhenTrue.Select(x => x.Item1).Should().Equal(30); + filter.CalledWhenFalse.Select(x => x.Item1).Should().Equal(10, 50, 20, 40); + + view.Select(x => x.Value).Should().Equal(30); + + filter.Clear(); + + set.Add(33); + set.AddRange(new[] { 98 }); + + filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Add, 33), (ChangedKind.Add, 98)); + filter.Clear(); + + set.Remove(10); + set.RemoveRange(new[] { 50, 30 }); + filter.CalledOnCollectionChanged.Select(x => (x.changedKind, x.value)).Should().Equal((ChangedKind.Remove, 10), (ChangedKind.Remove, 50), (ChangedKind.Remove, 30)); + } + } +}