4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】弱参照イベントを実装してみる

Posted at

はじめに

C# における通知パターンのひとつに、イベントがあります。

var nameChangedCount = 0;
var person = new Person("John");
person.PropertyChanged += (sender, e) =>
{
    if (e.PropertyName == "Name")
    {
        nameChangedCount++;
        Console.WriteLine($"Name changed count: {nameChangedCount}");
    }
};

person.Name = "George"; // Name changed count: 1

例えば ↑ のようなコードの場合、Name が変更されるたびに PropertyChanged イベントが発生し nameChangedCount が 1 ずつ増えていきます。personデリゲートを保持しているため person がメモリに残っている限り nameChangedCount 変数もメモリに残り続けます。つまり、nameChangedCountperson 以外から参照されていない状態でも GC によってメモリが回収されません。

殆どの場合この実装でも問題にはならないのですが、この手のオブジェクトの寿命に関する問題は色々と面倒です。きちんとやるなら適宜イベントを解除する必要があります。

person.PropertyChanged -= handler;

ただ、イベントを解除する適切なタイミングはプログラマが把握することは難しいため、GC に任せる必要があります。これには弱参照を使用します。これによってイベントうちっぱなしの弱参照イベントを作成します。

https://ufcpp.net/study/csharp/RmWeakReference.html

弱いイベントパターン は既にライブラリに存在するので、今回は勉強を兼ねて

  • リフレクションを使用しない(AoT ネイティブコンパイルでも使用できる)
  • 汎用的なイベントハンドラに使用できる

ことを目指します。

サンプルコード

弱参照イベント
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

/// <summary>
/// 弱参照イベントハンドラ
/// <br/> 使用例:
/// <code>
/// var weak = new WeakEventBase&lt;PropertyChangedEventHandler&gt;(n =&gt; {
///     return (sender, e) =&gt;
///         n.GetHandlerOrRemove()?.Invoke(sender, e);
/// },
///     (sender, e) =&gt; { /* DoAction */ },
///     remove =&gt; obj.PropertyChanged -= remove
/// );
/// </code>
/// </summary>
/// <typeparam name="T"></typeparam>
internal class WeakEventFunc<T> where T : Delegate
{
    private readonly Func<WeakEventFunc<T>, T> _invoker;
    private readonly WeakReference<T> _handler = new(null!);

    /// <summary>
    /// イベント実行ハンドラ
    /// </summary>
    [field: MaybeNull]
    public T Invoke => field ??= this._invoker(this);

    /// <summary>
    /// 登録されているイベントハンドラが開放されたときに発生するイベント
    /// </summary>
    public event Action<T>? Removed;

    /// <summary>
    /// 登録されているイベントハンドラ
    /// </summary>
    public T? Handler
    {
        get
        {
            if (this._handler.TryGetTarget(out var result))
                return result;
            return null;
        }
        set => this._handler.SetTarget(value!);
    }

    /// <summary>
    /// コンストラクタ
    /// <br/> 使用例:
    /// <code>
    /// var weak = new WeakEventBase&lt;PropertyChangedEventHandler&gt;(n =&gt; {
    ///     return (sender, e) =&gt;
    ///         n.GetHandlerOrRemove()?.Invoke(sender, e);
    /// },
    ///     (sender, e) =&gt; { /* DoAction */ },
    ///     remove =&gt; obj.PropertyChanged -= remove
    /// );
    /// </code>
    /// </summary>
    /// <param name="invoker"></param>
    /// <param name="handler"></param>
    /// <param name="removed"></param>
    /// <exception cref="ArgumentNullException"></exception>
    public WeakEventFunc(Func<WeakEventFunc<T>, T> invoker, T? handler = null, Action<T>? removed = null)
    {
        ArgumentNullException.ThrowIfNull(invoker);

        this._invoker = invoker;
        this.Handler = handler;
        this.Removed = removed;
    }

    /// <summary>
    /// 登録されているイベントハンドラを取得し、開放されていた場合は Removed イベントを発生させる
    /// </summary>
    /// <returns></returns>
    public T? GetHandlerOrRemove()
    {
        var result = this.Handler;
        if (result is null)
            this.Removed?.Invoke(this.Invoke);

        return result;
    }

    /// <summary>
    /// 暗黙の型変換
    /// </summary>
    /// <param name="act"></param>
    public static implicit operator T(WeakEventFunc<T> act) => act.Invoke;
}

/// <summary>
/// 弱参照イベントハンドラの dynamic 実装
/// 戻り値を持たないデリゲートのみサポートします
/// <br/> 使用例:
/// <code>
/// var weak = new WeakEventDynamic&lt;PropertyChangedEventHandler&gt;(n =&gt;
///     (sender, e) =&gt; { /* DoAction */ },
///     remove =&gt; obj.PropertyChanged -= remove
/// );
/// </code>
/// </summary>
/// <typeparam name="T"></typeparam>
internal class WeakEventDynamic<T> where T : Delegate
{
    private static bool? _isAction;
    private static bool IsAction => _isAction ??= typeof(T).GetMethod("Invoke")!.ReturnType == typeof(void);

    private readonly WeakReference<T> _handler = new WeakReference<T>(null!);

    /// <summary>
    /// イベントハンドラ
    /// </summary>
    [field: MaybeNull]
    public T Invoke => field ??= this.CreateInvokeMethod();

    /// <summary>
    /// 登録されているイベントハンドラ
    /// </summary>
    public T? Handler
    {
        get
        {
            if (this._handler.TryGetTarget(out var result))
                return result;
            return null;
        }
        set => this._handler.SetTarget(value!);
    }

    /// <summary>
    /// 登録されているイベントハンドラが開放されたときに発生するイベント
    /// </summary>
    public event Action<T>? Removed;

    /// <summary>
    /// コンストラクタ
    /// <br/> 使用例:
    /// <code>
    /// var weak = new WeakEventDynamic&lt;PropertyChangedEventHandler&gt;(n =&gt;
    ///     (sender, e) =&gt; { /* DoAction */ },
    ///     remove =&gt; obj.PropertyChanged -= remove
    /// );
    /// </code>
    /// </summary>
    /// <param name="handler"></param>
    /// <param name="removed"></param>
    /// <exception cref="InvalidOperationException"></exception>
    public WeakEventDynamic(T? handler = null, Action<T>? removed = null)
    {
        if (!IsAction)
            throw new InvalidOperationException("戻り値が void ではないデリゲートはサポートされていません。");

        this.Handler = handler;
        this.Removed = removed;
    }

    /// <summary>
    /// 登録されているイベントハンドラを取得し、開放されていた場合は Removed イベントを発生させる
    /// </summary>
    /// <returns></returns>
    public T? GetHandlerOrRemove()
    {
        var result = this.Handler;
        if (result is null)
            this.Removed?.Invoke(this.Invoke);

        return result;
    }

    /// <summary>
    /// 暗黙の型変換
    /// </summary>
    /// <param name="act"></param>
    public static implicit operator T(WeakEventDynamic<T> act) => act.Invoke;

    private T CreateInvokeMethod()
    {
        var info = typeof(T).GetMethod("Invoke")!;
        var paramsExprs = info.GetParameters().Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToArray();
        var getHandlerOrRemoveExpr = Expression.Call(Expression.Constant(this), typeof(WeakEventDynamic<T>).GetMethod(nameof(GetHandlerOrRemove))!);
        var handlerExpr = Expression.Variable(typeof(T), "handler");
        var assignExpr = Expression.Assign(handlerExpr, getHandlerOrRemoveExpr);
        var ifExpr = Expression.IfThen(
            Expression.NotEqual(handlerExpr, Expression.Constant(null)),
            Expression.Invoke(handlerExpr, paramsExprs)
        );
        var blockExpr = Expression.Block([handlerExpr], [assignExpr, ifExpr]);

        return Expression.Lambda<T>(blockExpr, paramsExprs).Compile();
    }
}

internal class 没クラス<T> where T : Delegate
{
    private readonly WeakReference<T> _handler = new(null!);
    private readonly WeakReference<Delegate> _action = new(null!);

    public T? Handler
    {
        get
        {
            if (this._handler.TryGetTarget(out var result))
                return result;
            return null;
        }
        set
        {
            this._handler.SetTarget(value!);
            this._action.SetTarget(null!);
        }
    }

    public void Invoke()
    {
        if (this._handler.TryGetTarget(out var handler))
        {
            Action? action = null;
            if (this._action.TryGetTarget(out var d))
                action = d as Action;
            if (action is null)
            {
                action = (Action)Delegate.CreateDelegate(typeof(Action), handler.Target, handler.Method);
                this._action.SetTarget(action);
            }
            action();
        }
    }

    public void Invoke<T1, T2>(T1 t1, T2 t2)
    {
        if (this._handler.TryGetTarget(out var handler))
        {
            Action<T1, T2>? action = null;
            if (this._action.TryGetTarget(out var d))
                action = d as Action<T1, T2>;
            if (action is null)
            {
                action = (Action<T1, T2>)Delegate.CreateDelegate(typeof(Action<T1, T2>), handler.Target, handler.Method);
                this._action.SetTarget(action);
            }
            action(t1, t2);
        }
    }
}
テストコード
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Xunit;

file class Person(string name) : INotifyPropertyChanged
{
    [field: MaybeNull]
    public static PropertyChangedEventArgs NameChangedEventArgs => field ??= new(nameof(Name));

    [field: MaybeNull]
    public string Name
    {
        get => field ?? "";
        set
        {
            field = value;
            this.PropertyChanged?.Invoke(this, NameChangedEventArgs);
        }
    } = name;

    public event PropertyChangedEventHandler? PropertyChanged;
}

file class EventListener(List<string> names)
{
    ~EventListener()
    {
        // 検証用のコード。通常はデストラクタで他のオブジェクトを参照するのはいかがかと思う
        names.Add("Finalized");
    }

    public void PropertyChangedHandler(object? sender, PropertyChangedEventArgs e) => names.Add(((Person)sender!).Name);
}

public class __WeakEventFuncTest
{
    [Fact]
    void HowToUse()
    {
        var person = new Person("John");
        var count = 0;
        person.PropertyChanged += new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        }, (sender, e) =>
        {
            count++;
        },
        remove =>
        {
            person.PropertyChanged -= remove;
        });

        person.Name = "George";
        Assert.Equal(1, count);

        person.Name = "Jane";
        Assert.Equal(2, count);
    }

    [Fact]
    void 登録した弱参照イベントがGCによって回収されること()
    {
        var person = new Person("John");
        var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        });
        person.PropertyChanged += weakEvent;
        var logs = new List<string>();

        // GC によって弱参照が回収されることを確認するために、処理を関数に分離
        void AddEvent()
        {
            var eventListener = new EventListener(logs);
            weakEvent.Handler += eventListener.PropertyChangedHandler;

            person.Name = "George";
            Assert.Equal(["George"], logs);
        }

        AddEvent();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        person.Name = "Jane";
        Assert.Equal(["George", "Finalized"], logs);
    }

    [Fact]
    void Constructor()
    {
        Assert.Throws<ArgumentNullException>(() => new WeakEventFunc<Action>(null!));

        var count = 0;
        Action handler = () => count++;
        var weak = new WeakEventFunc<Action>(n => () => n.GetHandlerOrRemove()?.Invoke(), handler);

        Assert.Equal(handler, weak.Handler);

        weak.Invoke();

        Assert.Equal(1, count);
    }

    [Fact]
    void Handler()
    {
        var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        });

        Assert.Equal(null!, weakEvent.Handler);
        PropertyChangedEventHandler handler1 = (sender, e) => { };
        PropertyChangedEventHandler handler2 = (sender, e) => { };

        weakEvent.Handler += handler1;
        Assert.Equal(handler1, weakEvent.Handler);

        weakEvent.Handler += handler2;
        Assert.Equal(handler1 + handler2, weakEvent.Handler);

        weakEvent.Handler -= handler1;
        Assert.Equal(handler2, weakEvent.Handler);
    }

    [Fact]
    void ImplicitOperator()
    {
        var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        });

        Assert.Equal(weakEvent.Invoke, weakEvent.Invoke);
    }

    [Fact]
    void 登録したイベントハンドラが実行されること()
    {
        var weakEvent = new WeakEventFunc<Action>(n =>
        {
            return () => n.GetHandlerOrRemove()?.Invoke();
        });

        var count = 0;
        Action handler = () => count++;
        weakEvent.Handler += handler;

        weakEvent.Invoke();
        Assert.Equal(1, count);
    }

    [Fact]
    void GetHandlerOrRemove()
    {
        var count = 0;
        var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => { };
        }, null, n =>
        {
            count++;
        });

        weakEvent.GetHandlerOrRemove();

        Assert.Equal(1, count);
    }

    [Fact]
    void Removed()
    {
        var person = new Person("John");
        var count = 0;
        var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        }, null, n =>
        {
            person.PropertyChanged -= n;
            count++;
        });
        person.PropertyChanged += weakEvent;
        var logs = new List<string>();

        // GC によって弱参照が回収されることを確認するために、処理を関数に分離
        void AddEvent()
        {
            var eventListener = new EventListener(logs);
            weakEvent.Handler += eventListener.PropertyChangedHandler;

            person.Name = "George";
            Assert.Equal(["George"], logs);
        }

        AddEvent();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        person.Name = "Jane";
        Assert.Equal(["George", "Finalized"], logs);

        Assert.Equal(1, count);
    }

    static void Performance(Performance p)
    {
        p.AddTest("Normal", () =>
        {
            var person = new Person("John");
            person.PropertyChanged += (sender, e) => { };
            for (var n = 0; n < 1000; ++n)
                person.Name = "George";
        });

        var personNormalLong = new Person("John");
        personNormalLong.PropertyChanged += (sender, e) => { };
        p.AddTest("NormalLong", () =>
        {
            for (var n = 0; n < 1000; ++n)
                personNormalLong.Name = "George";
        });

        p.AddTest("WeakFunc", () =>
        {
            var person = new Person("John");
            var weakEvent = new WeakEventFunc<PropertyChangedEventHandler>(n =>
            {
                return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
            });
            person.PropertyChanged += weakEvent;
            weakEvent.Handler += (sender, e) => { };
            for (var n = 0; n < 1000; ++n)
                person.Name = "George";
        });

        var personFuncLong = new Person("John");
        var weakEventFuncLong = new WeakEventFunc<PropertyChangedEventHandler>(n =>
        {
            return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
        });
        personFuncLong.PropertyChanged += weakEventFuncLong;
        p.AddTest("WeakFuncLong", () =>
        {
            for (var n = 0; n < 1000; ++n)
                personFuncLong.Name = "George";
        });

        p.AddTest("WeakDynamic", () =>
        {
            var person = new Person("John");
            var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>();
            person.PropertyChanged += weakEvent;
            weakEvent.Handler += (sender, e) => { };
            for (var n = 0; n < 1000; ++n)
                person.Name = "George";
        });

        var personDynamicLong = new Person("John");
        var weakEventDynamicLong = new WeakEventDynamic<PropertyChangedEventHandler>();
        personDynamicLong.PropertyChanged += weakEventDynamicLong;
        weakEventDynamicLong.Handler += (sender, e) => { };
        p.AddTest("WeakDynamicLong", () =>
        {
            for (var n = 0; n < 1000; ++n)
                personDynamicLong.Name = "George";
        });

        p.AddTest("没クラス", () =>
        {
            var person = new Person("John");
            var weakEvent = new 没クラス<PropertyChangedEventHandler>();
            person.PropertyChanged += weakEvent.Invoke;
            weakEvent.Handler += (sender, e) => { };
            for (var n = 0; n < 1000; ++n)
                person.Name = "George";
        });

        var personWeakLong = new Person("John");
        var weakEventWeakLong = new 没クラス<PropertyChangedEventHandler>();
        personWeakLong.PropertyChanged += weakEventWeakLong.Invoke;
        weakEventWeakLong.Handler += (sender, e) => { };
        p.AddTest("没クラスLong", () =>
        {
            for (var n = 0; n < 1000; ++n)
                personWeakLong.Name = "George";
        });
    }

    static void GetWeakReference(Performance p)
    {
        object strong = new object();
        p.AddTest("GetStrongReference", () =>
        {
            for (var n = 0; n < 10000; ++n)
                strong.GetHashCode();
        });

        var weak = new WeakReference<object>(strong);
        p.AddTest("GetWeakReference", () =>
        {
            for (var n = 0; n < 10000; ++n)
                if (weak.TryGetTarget(out var target))
                    target.GetHashCode();
        });

        var table = new System.Runtime.CompilerServices.ConditionalWeakTable<object, object>();
        table.Add(strong, strong);
        p.AddTest("GetWeakReferenceByTable", () =>
        {
            for (var n = 0; n < 10000; ++n)
                if (table.TryGetValue(strong, out var target))
                    target.GetHashCode();
        });
    }

    static void Description()
    {
        var nameChangedCount = 0;
        var person = new Person("John");
        person.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName == "Name")
            {
                nameChangedCount++;
                Console.WriteLine($"Name changed count: {nameChangedCount}");
            }
        };

        person.Name = "George"; // Name changed count: 1
    }

    [Fact]
    static void MultiCastDelegate()
    {
        var number = 0;
        var weak = new WeakReference<Action>(null!);
        Action action1 = () => number = 1;

        void AddEvent()
        {
            Action action2 = () => number = 2;

            weak.SetTarget(action1 + action2);

            weak.TryGetTarget(out var target);
            target!.Invoke();
        }

        AddEvent();
        Assert.Equal(2, number);

        GC.Collect();
        GC.WaitForPendingFinalizers();

        Assert.False(weak.TryGetTarget(out _));
    }
}

public class __WeakEventDynamicTest
{
    [Fact]
    void HowToUse()
    {
        var person = new Person("John");
        var count = 0;
        person.PropertyChanged += new WeakEventDynamic<PropertyChangedEventHandler>((sender, e) =>
        {
            count++;
        }, remove =>
        {
            person.PropertyChanged -= remove;
        });

        person.Name = "George";
        Assert.Equal(1, count);

        person.Name = "Jane";
        Assert.Equal(2, count);
    }

    [Fact]
    void 登録した弱参照イベントがGCによって回収されること()
    {
        var person = new Person("John");
        var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>();
        person.PropertyChanged += weakEvent;
        var logs = new List<string>();

        // GC によって弱参照が回収されることを確認するために、処理を関数に分離
        void AddEvent()
        {
            var eventListener = new EventListener(logs);
            weakEvent.Handler += eventListener.PropertyChangedHandler;

            person.Name = "George";
            Assert.Equal(["George"], logs);
        }

        AddEvent();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        person.Name = "Jane";
        Assert.Equal(["George", "Finalized"], logs);
    }

    [Fact]
    void Constructor()
    {
        Assert.Throws<InvalidOperationException>(() => new WeakEventDynamic<Func<bool>>(null));

        var count = 0;
        Action handler = () => count++;
        var weak = new WeakEventDynamic<Action>(handler);

        Assert.Equal(handler, weak.Handler);

        weak.Invoke();

        Assert.Equal(1, count);
    }

    [Fact]
    void Handler()
    {
        var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>();

        Assert.Equal(null!, weakEvent.Handler);
        PropertyChangedEventHandler handler1 = (sender, e) => { };
        PropertyChangedEventHandler handler2 = (sender, e) => { };

        weakEvent.Handler += handler1;
        Assert.Equal(handler1, weakEvent.Handler);

        weakEvent.Handler += handler2;
        Assert.Equal(handler1 + handler2, weakEvent.Handler);

        weakEvent.Handler -= handler1;
        Assert.Equal(handler2, weakEvent.Handler);
    }

    [Fact]
    void GetHandlerOrRemove()
    {
        var count = 0;
        var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>(null, n =>
        {
            count++;
        });

        weakEvent.GetHandlerOrRemove();

        Assert.Equal(1, count);
    }

    [Fact]
    void Removed()
    {
        var person = new Person("John");
        var count = 0;
        var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>(null, n =>
        {
            person.PropertyChanged -= n;
            count++;
        });
        person.PropertyChanged += weakEvent;
        var logs = new List<string>();

        // GC によって弱参照が回収されることを確認するために、処理を関数に分離
        void AddEvent()
        {
            var eventListener = new EventListener(logs);
            weakEvent.Handler += eventListener.PropertyChangedHandler;

            person.Name = "George";
            Assert.Equal(["George"], logs);
        }

        AddEvent();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        person.Name = "Jane";
        Assert.Equal(["George", "Finalized"], logs);

        Assert.Equal(1, count);
    }

    [Fact]
    void ImplicitOperator()
    {
        var weakEvent = new WeakEventDynamic<PropertyChangedEventHandler>();

        Assert.Equal(weakEvent.Invoke, weakEvent.Invoke);
    }

    [Fact]
    void Invoke_イベントが実行されること()
    {
        var weakEvent = new WeakEventDynamic<Action>();

        var count = 0;
        weakEvent.Handler += () => count++;

        weakEvent.Invoke();
        Assert.Equal(1, count);
    }

    [Fact]
    void Invoke_デリゲートに渡される引数が正しいこと()
    {
        var weakEvent = new WeakEventDynamic<Action<int, string>>();

        int got1 = 0;
        string? got2 = null;
        weakEvent.Handler += (num1, num2) =>
        {
            got1 = num1;
            got2 = num2;
        };

        weakEvent.Invoke(1, "a");
        Assert.Equal(1, got1);
        Assert.Equal("a", got2);
    }
}

使い方

var person = new Person("John");
var count = 0;
person.PropertyChanged += new WeakEventFunc<PropertyChangedEventHandler>(n =>
{
    return (sender, e) => n.GetHandlerOrRemove()?.Invoke(sender, e);
}, (sender, e) =>
{
    count++;
},
remove =>
{
    person.PropertyChanged -= remove;
});

person.Name = "George";
Assert.Equal(1, count);

person.Name = "Jane";
Assert.Equal(2, count);

リフレクション使用版

リフレクションを使用しないといってはみたものの、引数を省略できるリフレクション使用版も用意しました。パフォーマンスが悪いです(後述)。

リフレクション使用版
var person = new Person("John");
var count = 0;
person.PropertyChanged += new WeakEventDynamic<PropertyChangedEventHandler>((sender, e) =>
{
    count++;
}, remove =>
{
    person.PropertyChanged -= remove;
});

パフォーマンス

Test Score % CG0
Normal 6,725 100.0% 0
NormalLong 26,513 394.2% 0
WeakFunc 4,038 60.0% 0
WeakFuncLong 4,035 60.0% 0
WeakDynamic 249 3.7% 0
WeakDynamicLong 5,309 78.9% 0

実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。

  • 通常のイベントパターンと比較すると、弱参照イベントパターンはパフォーマンスが悪いです
  • とはいえ致命的な遅さ(1% とか)ではないため、実用的です
  • WeakDynamic はインスタンスごとに式木をコンパイルするためパフォーマンスが悪いです
    • 大量のインスタンスにイベントを紐づけるような使い方には向かなそうです
    • 少量の寿命が長いインスタンスを使うような場合(例:ロガーに書き込むとか)はそこまでパフォーマンスが悪くないです

気になるところ

デリゲートを返すデリゲートの複雑さ

コンストラクタの第1引数の

(WeakEventFunc<PropertyChangedEventHandler> weak) =>
{
    return (object? sender, PropertyChangedEventArgs e) =>
        weak.GetHandlerOrRemove()?.Invoke(sender, e);
}

はデリゲートを返すデリゲートになっていて、少々わかりにくいです。いわゆる高階関数は複雑になりがちで、この例では weak を束縛変数としたクロージャ生成 https://ufcpp.net/study/csharp/sp2_anonymousmethod.html#closure も行っています。

この部分を省略できないか色々と考えてみましたが、リフレクションなしだと手書きで毎回このコードを書く必要がありそうです。

イベント解除用のコードを手書きするのが面倒

コンストラクタの引数(オプション)にイベント解除用のイベントを指定できるようにしました。

remove =>
{
    person.PropertyChanged -= remove;
}

イベントを解除しなくても弱参照イベントに登録したオブジェクトは回収されるため、これは省略しても問題はあまりありません。しかしながら副作用があまりないとはいえメモリリークなので、解除できるようにしました。

演算子の多重定義はどうなん

person.PropertyChanged += new WeakEventFunc<PropertyChangedEventHandler>...

// これは ↓ の糖衣構文
weak = new WeakEventFunc<PropertyChangedEventHandler>...
person.PropertyChanged += weak.Invoke;

演算子の多重定義は、コードを一瞥したときに挙動がわかりにくくなるため避けるべきだと思います。
しかしながら今回は弱参照イベントということで、利便性を優先し変数宣言を省略して気軽に使えるようにしました。まあ見た感じそこまでわかりにくくもないかなと思います。

static なイベント

WPF でレンダリングを制御したいときに、↓ のようなコードを書くことがあります。

System.Windows.Media.CompositionTarget.Rendering += (_, _) => DoWork();

static なイベントは GC によってメモリ削除されません。例えば特定のウィンドウのレンダリングを紐づけたとき、そのウィンドウが閉じられたあともずっと参照され続けます。ウィンドウはアプリが終了するまで開放されません。

こういったときに弱参照イベントパターンを使うと、イベント解除を考慮する必要がなくなって良さげです。

ボツ案

今回はなかなか思ったように実装できませんでした。いくつかボツ案を書き留めておきます。

ボツ案
  • System.Runtime.CompilerServices.ConditionalWeakTable を使うパターン:パフォーマンスが悪い、10% くらいしか出ないため没
  • WeakEvent.Invoke() のオーバーロードをいくつか用意する:
    • += でデリゲート登録のとき型推論とオーバーロード解決できるためやれないことはない
    • 40% くらいでパフォーマンスが出ないのと、リフレクション必須になるため没
    • 型推論とオーバーロード解決はなにかに役立ちそうな知見
強参照、弱参照、ConditionalWeakTable のパフォーマンス比較
Test Score % CG0
GetStrongReference 3,591 100.0% 0
GetWeakReference 2,552 71.1% 0
GetWeakReferenceByTable 503 14.0% 0

おわりに

今回はあまりスマートとは言えないものの、弱参照イベントを実装しました。何かいい方法を思いついたら更新しようと思います。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?