diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs index e41e1cc488..611cc23992 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// public class Classes : AvaloniaList, IPseudoClasses { - private SafeEnumerableList? _listeners; + private SafeEnumerableHashSet? _listeners; /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs b/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs new file mode 100644 index 0000000000..07d56c8f0f --- /dev/null +++ b/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// Implements a simple set which is safe to modify during enumeration. + /// + /// The item type. + /// + /// Implements a set which, when written to while enumerating, performs a copy of the set + /// items. Note this class doesn't actually implement as it's not + /// currently needed - feel free to add missing methods etc. + /// + internal class SafeEnumerableHashSet : IEnumerable + { + private HashSet _hashSet = new(); + private int _generation; + private int _enumCount = 0; + + public int Count => _hashSet.Count; + internal HashSet Inner => _hashSet; + + public void Add(T item) => GetSet().Add(item); + public bool Remove(T item) => GetSet().Remove(item); + + public Enumerator GetEnumerator() => new(this, _hashSet); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private HashSet GetSet() + { + if (_enumCount > 0) + { + // .NET has a fastpath for cloning a hashset when passed in via the constructor + _hashSet = new(_hashSet); + ++_generation; + _enumCount = 0; + } + + return _hashSet; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly SafeEnumerableHashSet _owner; + private readonly int _generation; + private HashSet.Enumerator _enumerator; + + internal Enumerator(SafeEnumerableHashSet owner, HashSet list) + { + _owner = owner; + _generation = owner._generation; + ++_owner._enumCount; + _enumerator = list.GetEnumerator(); + } + + public void Dispose() + { + _enumerator.Dispose(); + if (_owner._generation == _generation) + --_owner._enumCount; + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public T Current => _enumerator.Current; + object? IEnumerator.Current => _enumerator.Current; + + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs deleted file mode 100644 index dd437d27be..0000000000 --- a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace Avalonia.Utilities -{ - /// - /// Implements a simple list which is safe to modify during enumeration. - /// - /// The item type. - /// - /// Implements a list which, when written to while enumerating, performs a copy of the list - /// items. Note this this class doesn't actually implement as it's not - /// currently needed - feel free to add missing methods etc. - /// - internal class SafeEnumerableList : IEnumerable - { - private List _list = new(); - private int _generation; - private int _enumCount = 0; - - public int Count => _list.Count; - internal List 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 IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private List GetList() - { - if (_enumCount > 0) - { - _list = new(_list); - ++_generation; - _enumCount = 0; - } - - return _list; - } - - public struct Enumerator : IEnumerator, IEnumerator - { - private readonly SafeEnumerableList _owner; - private readonly List _list; - private readonly int _generation; - private int _index; - private T? _current; - - internal Enumerator(SafeEnumerableList owner, List 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; - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableHashSetTests.cs similarity index 68% rename from tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs rename to tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableHashSetTests.cs index f48bde4731..022ed52685 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableHashSetTests.cs @@ -4,12 +4,12 @@ using Xunit; namespace Avalonia.Controls.UnitTests.Utils { - public class SafeEnumerableListTests + public class SafeEnumerableHashSetTests { [Fact] - public void List_Is_Not_Copied_Outside_Enumeration() + public void Set_Is_Not_Copied_Outside_Enumeration() { - var target = new SafeEnumerableList(); + var target = new SafeEnumerableHashSet(); var inner = target.Inner; target.Add("foo"); @@ -20,9 +20,9 @@ namespace Avalonia.Controls.UnitTests.Utils } [Fact] - public void List_Is_Copied_Outside_Enumeration() + public void Set_Is_Copied_Outside_Enumeration() { - var target = new SafeEnumerableList(); + var target = new SafeEnumerableHashSet(); var inner = target.Inner; target.Add("foo"); @@ -43,13 +43,13 @@ namespace Avalonia.Controls.UnitTests.Utils Assert.NotSame(inner, target.Inner); } - Assert.Equal(new[] { "foo", "bar", "baz", "baz" }, target); + Assert.Equal(new HashSet { "foo", "bar", "baz", "baz" }, target); } [Fact] - public void List_Is_Not_Copied_After_Enumeration() + public void Set_Is_Not_Copied_After_Enumeration() { - var target = new SafeEnumerableList(); + var target = new SafeEnumerableHashSet(); var inner = target.Inner; target.Add("foo"); @@ -67,9 +67,9 @@ namespace Avalonia.Controls.UnitTests.Utils } [Fact] - public void List_Is_Copied_Only_Once_During_Enumeration() + public void Set_Is_Copied_Only_Once_During_Enumeration() { - var target = new SafeEnumerableList(); + var target = new SafeEnumerableHashSet(); var inner = target.Inner; target.Add("foo"); @@ -87,14 +87,14 @@ namespace Avalonia.Controls.UnitTests.Utils } [Fact] - public void List_Is_Copied_During_Nested_Enumerations() + public void Set_Is_Copied_During_Nested_Enumerations() { - var target = new SafeEnumerableList(); + var target = new SafeEnumerableHashSet(); var initialInner = target.Inner; - var firstItems = new List(); - var secondItems = new List(); - List firstInner; - List secondInner; + var firstItems = new HashSet(); + var secondItems = new HashSet(); + HashSet firstInner; + HashSet secondInner; target.Add("foo"); @@ -118,9 +118,9 @@ namespace Avalonia.Controls.UnitTests.Utils firstItems.Add(i); } - Assert.Equal(new[] { "foo" }, firstItems); - Assert.Equal(new[] { "foo", "bar" }, secondItems); - Assert.Equal(new[] { "foo", "bar", "baz", "baz" }, target); + Assert.Equal(new HashSet { "foo" }, firstItems); + Assert.Equal(new HashSet { "foo", "bar" }, secondItems); + Assert.Equal(new HashSet { "foo", "bar", "baz", "baz" }, target); var finalInner = target.Inner; target.Add("final");