Browse Source
Adds a simple copy-on-write list and uses it to store the listeners to `Classes` to prevent problems with re-entrancy. Fixes #8480pull/8492/head
3 changed files with 221 additions and 3 deletions
@ -0,0 +1,89 @@ |
|||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Implements a simple list which is safe to modify during enumeration.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The item type.</typeparam>
|
||||
|
/// <remarks>
|
||||
|
/// Implements a list which, when written to while enumerating, performs a copy of the list
|
||||
|
/// items. Note this this class doesn't actually implement <see cref="IList{T}"/> as it's not
|
||||
|
/// currently needed - feel free to add missing methods etc.
|
||||
|
/// </remarks>
|
||||
|
internal class SafeEnumerableList<T> : IEnumerable<T> |
||||
|
{ |
||||
|
private List<T> _list = new(); |
||||
|
private int _generation; |
||||
|
private int _enumCount = 0; |
||||
|
|
||||
|
public int Count => _list.Count; |
||||
|
internal List<T> Inner => _list; |
||||
|
|
||||
|
public void Add(T item) => GetList().Add(item); |
||||
|
public bool Remove(T item) => GetList().Remove(item); |
||||
|
|
||||
|
public Enumerator GetEnumerator() => new(this, _list); |
||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); |
||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
|
||||
|
private List<T> GetList() |
||||
|
{ |
||||
|
if (_enumCount > 0) |
||||
|
{ |
||||
|
_list = new(_list); |
||||
|
++_generation; |
||||
|
_enumCount = 0; |
||||
|
} |
||||
|
|
||||
|
return _list; |
||||
|
} |
||||
|
|
||||
|
public struct Enumerator : IEnumerator<T>, IEnumerator |
||||
|
{ |
||||
|
private readonly SafeEnumerableList<T> _owner; |
||||
|
private readonly List<T> _list; |
||||
|
private readonly int _generation; |
||||
|
private int _index; |
||||
|
private T? _current; |
||||
|
|
||||
|
internal Enumerator(SafeEnumerableList<T> owner, List<T> list) |
||||
|
{ |
||||
|
_owner = owner; |
||||
|
_list = list; |
||||
|
_generation = owner._generation; |
||||
|
_index = 0; |
||||
|
_current = default; |
||||
|
++_owner._enumCount; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_owner._generation == _generation) |
||||
|
--_owner._enumCount; |
||||
|
} |
||||
|
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
if (_index < _list.Count) |
||||
|
{ |
||||
|
_current = _list[_index++]; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
_current = default; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public T Current => _current!; |
||||
|
object? IEnumerator.Current => _current; |
||||
|
|
||||
|
void IEnumerator.Reset() |
||||
|
{ |
||||
|
_index = 0; |
||||
|
_current = default; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Utilities; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests.Utils |
||||
|
{ |
||||
|
public class SafeEnumerableListTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void List_Is_Not_Copied_Outside_Enumeration() |
||||
|
{ |
||||
|
var target = new SafeEnumerableList<string>(); |
||||
|
var inner = target.Inner; |
||||
|
|
||||
|
target.Add("foo"); |
||||
|
target.Add("bar"); |
||||
|
target.Remove("foo"); |
||||
|
|
||||
|
Assert.Same(inner, target.Inner); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void List_Is_Copied_Outside_Enumeration() |
||||
|
{ |
||||
|
var target = new SafeEnumerableList<string>(); |
||||
|
var inner = target.Inner; |
||||
|
|
||||
|
target.Add("foo"); |
||||
|
|
||||
|
foreach (var i in target) |
||||
|
{ |
||||
|
Assert.Same(inner, target.Inner); |
||||
|
target.Add("bar"); |
||||
|
Assert.NotSame(inner, target.Inner); |
||||
|
Assert.Equal("foo", i); |
||||
|
} |
||||
|
|
||||
|
inner = target.Inner; |
||||
|
|
||||
|
foreach (var i in target) |
||||
|
{ |
||||
|
target.Add("baz"); |
||||
|
Assert.NotSame(inner, target.Inner); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(new[] { "foo", "bar", "baz", "baz" }, target); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void List_Is_Not_Copied_After_Enumeration() |
||||
|
{ |
||||
|
var target = new SafeEnumerableList<string>(); |
||||
|
var inner = target.Inner; |
||||
|
|
||||
|
target.Add("foo"); |
||||
|
|
||||
|
foreach (var i in target) |
||||
|
{ |
||||
|
target.Add("bar"); |
||||
|
Assert.NotSame(inner, target.Inner); |
||||
|
inner = target.Inner; |
||||
|
Assert.Equal("foo", i); |
||||
|
} |
||||
|
|
||||
|
target.Add("baz"); |
||||
|
Assert.Same(inner, target.Inner); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void List_Is_Copied_Only_Once_During_Enumeration() |
||||
|
{ |
||||
|
var target = new SafeEnumerableList<string>(); |
||||
|
var inner = target.Inner; |
||||
|
|
||||
|
target.Add("foo"); |
||||
|
|
||||
|
foreach (var i in target) |
||||
|
{ |
||||
|
target.Add("bar"); |
||||
|
Assert.NotSame(inner, target.Inner); |
||||
|
inner = target.Inner; |
||||
|
target.Add("baz"); |
||||
|
Assert.Same(inner, target.Inner); |
||||
|
} |
||||
|
|
||||
|
target.Add("baz"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void List_Is_Copied_During_Nested_Enumerations() |
||||
|
{ |
||||
|
var target = new SafeEnumerableList<string>(); |
||||
|
var initialInner = target.Inner; |
||||
|
var firstItems = new List<string>(); |
||||
|
var secondItems = new List<string>(); |
||||
|
List<string> firstInner; |
||||
|
List<string> secondInner; |
||||
|
|
||||
|
target.Add("foo"); |
||||
|
|
||||
|
foreach (var i in target) |
||||
|
{ |
||||
|
target.Add("bar"); |
||||
|
|
||||
|
firstInner = target.Inner; |
||||
|
Assert.NotSame(initialInner, firstInner); |
||||
|
|
||||
|
foreach (var j in target) |
||||
|
{ |
||||
|
target.Add("baz"); |
||||
|
|
||||
|
secondInner = target.Inner; |
||||
|
Assert.NotSame(firstInner, secondInner); |
||||
|
|
||||
|
secondItems.Add(j); |
||||
|
} |
||||
|
|
||||
|
firstItems.Add(i); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(new[] { "foo" }, firstItems); |
||||
|
Assert.Equal(new[] { "foo", "bar" }, secondItems); |
||||
|
Assert.Equal(new[] { "foo", "bar", "baz", "baz" }, target); |
||||
|
|
||||
|
var finalInner = target.Inner; |
||||
|
target.Add("final"); |
||||
|
Assert.Same(finalInner, target.Inner); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue