This commit is contained in:
neuecc 2021-08-18 19:27:47 +09:00
parent 2a85ffdac8
commit d7d621ab0d
9 changed files with 436 additions and 16 deletions

View File

@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
namespace ObservableCollections.Internal namespace ObservableCollections.Internal
{ {
@ -69,7 +70,7 @@ namespace ObservableCollections.Internal
{ {
if (array.Length == index) if (array.Length == index)
{ {
ArrayPool<T>.Shared.Return(array); ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
} }
array = ArrayPool<T>.Shared.Rent(index * 2); array = ArrayPool<T>.Shared.Rent(index * 2);
} }
@ -78,7 +79,7 @@ namespace ObservableCollections.Internal
{ {
if (array != null) if (array != null)
{ {
ArrayPool<T>.Shared.Return(array); ArrayPool<T>.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
array = null; array = null;
} }
} }

View File

@ -346,7 +346,8 @@ namespace ObservableCollections.Internal
var value = e.OldItem; var value = e.OldItem;
var key = keySelector(value); var key = keySelector(value);
lookup // TODO:...
// lookup
//var removeItems = lookup[key]; //var removeItems = lookup[key];
//foreach (var v in removeItems) //foreach (var v in removeItems)

View 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");
}
}
}

View File

@ -1,14 +1,162 @@
using System.Collections.Generic; using ObservableCollections.Internal;
using System.Collections;
using System.Collections.Specialized;
namespace ObservableCollections namespace ObservableCollections
{ {
public sealed partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T> public sealed partial class ObservableHashSet<T> : IReadOnlyCollection<T>, IObservableCollection<T>
{ {
// TODO: public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool _ = false)
public ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = 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);
}
}
} }
} }
} }

View File

@ -1,11 +1,12 @@
using ObservableCollections.Internal; using ObservableCollections.Internal;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
namespace ObservableCollections namespace ObservableCollections
{ {
// can not implements ISet<T> because set operation can not get added/removed values. // 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> public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
where T : notnull
{ {
readonly HashSet<T> set; readonly HashSet<T> set;
public object SyncRoot { get; } = new object(); public object SyncRoot { get; } = new object();
@ -40,8 +41,143 @@ namespace ObservableCollections
public bool IsReadOnly => false; 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) public bool Contains(T item)
{ {

View File

@ -11,12 +11,12 @@ namespace ObservableCollections
return new View<TView>(this, transform, reverse); 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 ObservableList<T> source;
readonly Func<T, TView> selector; readonly Func<T, TView> selector;
readonly bool reverse; readonly bool reverse;
protected readonly List<(T, TView)> list; readonly List<(T, TView)> list;
ISynchronizedViewFilter<T, TView> filter; ISynchronizedViewFilter<T, TView> filter;
@ -111,8 +111,6 @@ namespace ObservableCollections
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
list.EnsureCapacity(e.NewItems.Length);
// Add // Add
if (e.NewStartingIndex == list.Count) if (e.NewStartingIndex == list.Count)
{ {

View File

@ -2,6 +2,7 @@
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ObservableCollections namespace ObservableCollections
{ {
@ -129,7 +130,7 @@ namespace ObservableCollections
} }
finally finally
{ {
ArrayPool<T>.Shared.Return(dest); ArrayPool<T>.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
} }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace ObservableCollections namespace ObservableCollections
{ {
@ -126,7 +127,7 @@ namespace ObservableCollections
} }
finally finally
{ {
ArrayPool<T>.Shared.Return(dest); ArrayPool<T>.Shared.Return(dest, RuntimeHelpers.IsReferenceOrContainsReferences<T>());
} }
} }
} }

View 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));
}
}
}