Skip to content

Commit

Permalink
Switch to a HashSet<T> as backing for SafeEnumerableList (AvaloniaUI#…
Browse files Browse the repository at this point in the history
…16633)

* Switch to a set as backing for _listeners on Classes. Via the `SafeEnumerableHashSet`

* Update docs
  • Loading branch information
halgari authored Aug 13, 2024
1 parent ec655a9 commit eb5f395
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 109 deletions.
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Controls/Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Avalonia.Controls
/// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses
{
private SafeEnumerableList<IClassesChangedListener>? _listeners;
private SafeEnumerableHashSet<IClassesChangedListener>? _listeners;

/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
Expand Down
80 changes: 80 additions & 0 deletions src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Avalonia.Utilities
{
/// <summary>
/// Implements a simple set which is safe to modify during enumeration.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <remarks>
/// Implements a set which, when written to while enumerating, performs a copy of the set
/// items. Note this class doesn't actually implement <see cref="ISet{T}"/> as it's not
/// currently needed - feel free to add missing methods etc.
/// </remarks>
internal class SafeEnumerableHashSet<T> : IEnumerable<T>
{
private HashSet<T> _hashSet = new();
private int _generation;
private int _enumCount = 0;

public int Count => _hashSet.Count;
internal HashSet<T> 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<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private HashSet<T> 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<T>, IEnumerator
{
private readonly SafeEnumerableHashSet<T> _owner;
private readonly int _generation;
private HashSet<T>.Enumerator _enumerator;

internal Enumerator(SafeEnumerableHashSet<T> owner, HashSet<T> 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();
}
}
}
}
89 changes: 0 additions & 89 deletions src/Avalonia.Base/Utilities/SafeEnumerableList.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

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<string>();
var target = new SafeEnumerableHashSet<string>();
var inner = target.Inner;

target.Add("foo");
Expand All @@ -20,9 +20,9 @@ public void List_Is_Not_Copied_Outside_Enumeration()
}

[Fact]
public void List_Is_Copied_Outside_Enumeration()
public void Set_Is_Copied_Outside_Enumeration()
{
var target = new SafeEnumerableList<string>();
var target = new SafeEnumerableHashSet<string>();
var inner = target.Inner;

target.Add("foo");
Expand All @@ -43,13 +43,13 @@ public void List_Is_Copied_Outside_Enumeration()
Assert.NotSame(inner, target.Inner);
}

Assert.Equal(new[] { "foo", "bar", "baz", "baz" }, target);
Assert.Equal(new HashSet<string> { "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<string>();
var target = new SafeEnumerableHashSet<string>();
var inner = target.Inner;

target.Add("foo");
Expand All @@ -67,9 +67,9 @@ public void List_Is_Not_Copied_After_Enumeration()
}

[Fact]
public void List_Is_Copied_Only_Once_During_Enumeration()
public void Set_Is_Copied_Only_Once_During_Enumeration()
{
var target = new SafeEnumerableList<string>();
var target = new SafeEnumerableHashSet<string>();
var inner = target.Inner;

target.Add("foo");
Expand All @@ -87,14 +87,14 @@ public void List_Is_Copied_Only_Once_During_Enumeration()
}

[Fact]
public void List_Is_Copied_During_Nested_Enumerations()
public void Set_Is_Copied_During_Nested_Enumerations()
{
var target = new SafeEnumerableList<string>();
var target = new SafeEnumerableHashSet<string>();
var initialInner = target.Inner;
var firstItems = new List<string>();
var secondItems = new List<string>();
List<string> firstInner;
List<string> secondInner;
var firstItems = new HashSet<string>();
var secondItems = new HashSet<string>();
HashSet<string> firstInner;
HashSet<string> secondInner;

target.Add("foo");

Expand All @@ -118,9 +118,9 @@ public void List_Is_Copied_During_Nested_Enumerations()
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<string> { "foo" }, firstItems);
Assert.Equal(new HashSet<string> { "foo", "bar" }, secondItems);
Assert.Equal(new HashSet<string> { "foo", "bar", "baz", "baz" }, target);

var finalInner = target.Inner;
target.Add("final");
Expand Down

0 comments on commit eb5f395

Please sign in to comment.