Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to a HashSet<T> as backing for SafeEnumerableList #16633

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading