HashSet
This commit is contained in:
parent
2a85ffdac8
commit
d7d621ab0d
@ -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<T>.Shared.Return(array);
|
||||
ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
}
|
||||
array = ArrayPool<T>.Shared.Rent(index * 2);
|
||||
}
|
||||
@ -78,7 +79,7 @@ namespace ObservableCollections.Internal
|
||||
{
|
||||
if (array != null)
|
||||
{
|
||||
ArrayPool<T>.Shared.Return(array);
|
||||
ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
array = null;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
54
src/ObservableCollections/Internal/ResizableArray.cs
Normal file
54
src/ObservableCollections/Internal/ResizableArray.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ObservableCollections.Internal
|
||||
{
|
||||
internal ref struct ResizableArray<T>
|
||||
{
|
||||
T[]? array;
|
||||
int count;
|
||||
|
||||
public ReadOnlySpan<T> Span => array.AsSpan(0, count);
|
||||
|
||||
public ResizableArray(int initialCapacity)
|
||||
{
|
||||
array = ArrayPool<T>.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<T>.Shared.Return(array!, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
array = newArray;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (array != null)
|
||||
{
|
||||
ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
array = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
void Throw()
|
||||
{
|
||||
throw new ObjectDisposedException("ResizableArray");
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T> : IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
{
|
||||
// TODO:
|
||||
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false)
|
||||
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool _ = false)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return new View<TView>(this, transform);
|
||||
}
|
||||
|
||||
sealed class View<TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
readonly ObservableHashSet<T> source;
|
||||
readonly Func<T, TView> selector;
|
||||
readonly Dictionary<T, (T, TView)> dict;
|
||||
|
||||
ISynchronizedViewFilter<T, TView> filter;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
|
||||
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
|
||||
|
||||
public object SyncRoot { get; }
|
||||
|
||||
public View(ObservableHashSet<T> source, Func<T, TView> selector)
|
||||
{
|
||||
this.source = source;
|
||||
this.selector = selector;
|
||||
this.filter = SynchronizedViewFilter<T, TView>.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<T, TView> filter)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = filter;
|
||||
foreach (var (_, (value, view)) in dict)
|
||||
{
|
||||
filter.InvokeOnAttach(value, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFilter(Action<T, TView>? resetAction)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
this.filter = SynchronizedViewFilter<T, TView>.Null;
|
||||
if (resetAction != null)
|
||||
{
|
||||
foreach (var (_, (value, view)) in dict)
|
||||
{
|
||||
resetAction(value, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<(T, TView)> GetEnumerator()
|
||||
{
|
||||
return new SynchronizedViewEnumerator<T, TView>(SyncRoot, dict.Select(x => x.Value).GetEnumerator(), filter);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.source.CollectionChanged -= SourceCollectionChanged;
|
||||
}
|
||||
|
||||
private void SourceCollectionChanged(in NotifyCollectionChangedEventArgs<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
using ObservableCollections.Internal;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ObservableCollections
|
||||
{
|
||||
// can not implements ISet<T> because set operation can not get added/removed values.
|
||||
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
|
||||
where T : notnull
|
||||
{
|
||||
readonly HashSet<T> 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<T>.Add(item, -1));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<T> items)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!items.TryGetNonEnumeratedCount(out var capacity))
|
||||
{
|
||||
capacity = 4;
|
||||
}
|
||||
|
||||
using (var list = new ResizableArray<T>(capacity))
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (set.Add(item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(list.Span, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRange(T[] items)
|
||||
{
|
||||
AddRange(items.AsSpan());
|
||||
}
|
||||
|
||||
public void AddRange(ReadOnlySpan<T> items)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
using (var list = new ResizableArray<T>(items.Length))
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (set.Add(item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Add(list.Span, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (set.Remove(item))
|
||||
{
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(item, -1));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveRange(IEnumerable<T> items)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (!items.TryGetNonEnumeratedCount(out var capacity))
|
||||
{
|
||||
capacity = 4;
|
||||
}
|
||||
|
||||
using (var list = new ResizableArray<T>(capacity))
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (set.Remove(item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(list.Span, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveRange(T[] items)
|
||||
{
|
||||
RemoveRange(items.AsSpan());
|
||||
}
|
||||
|
||||
public void RemoveRange(ReadOnlySpan<T> items)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
using (var list = new ResizableArray<T>(items.Length))
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (set.Remove(item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Remove(list.Span, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
set.Clear();
|
||||
CollectionChanged?.Invoke(NotifyCollectionChangedEventArgs<T>.Reset());
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
|
||||
{
|
||||
return set.TryGetValue(equalValue, out actualValue);
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
|
@ -11,12 +11,12 @@ namespace ObservableCollections
|
||||
return new View<TView>(this, transform, reverse);
|
||||
}
|
||||
|
||||
class View<TView> : ISynchronizedView<T, TView>
|
||||
sealed class View<TView> : ISynchronizedView<T, TView>
|
||||
{
|
||||
readonly ObservableList<T> source;
|
||||
readonly Func<T, TView> selector;
|
||||
readonly bool reverse;
|
||||
protected readonly List<(T, TView)> list;
|
||||
readonly List<(T, TView)> list;
|
||||
|
||||
ISynchronizedViewFilter<T, TView> filter;
|
||||
|
||||
@ -111,8 +111,6 @@ namespace ObservableCollections
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
list.EnsureCapacity(e.NewItems.Length);
|
||||
|
||||
// Add
|
||||
if (e.NewStartingIndex == list.Count)
|
||||
{
|
||||
|
@ -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<T>.Shared.Return(dest);
|
||||
ArrayPool<T>.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T>.Shared.Return(dest);
|
||||
ArrayPool<T>.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
80
tests/ObservableCollections.Tests/ObservableHashSetTest.cs
Normal file
80
tests/ObservableCollections.Tests/ObservableHashSetTest.cs
Normal file
@ -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<int>();
|
||||
var view = set.CreateView(x => new ViewContainer<int>(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<int>();
|
||||
var view = set.CreateView(x => new ViewContainer<int>(x));
|
||||
var filter = new TestFilter<int>((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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user