WeakDelegateによるイベントメモリリーク対策の実装

前回の記事ではイベントによるメモリリーク問題と弱参照(WeakReference)を応用した解決策について触れました。今回は具体的なWeakDelegateの実装方法について詳細に説明します。

イベントの参照メカニズム

.NETのイベントはデフォルトでサブスクライバーに対して強参照を保持します。この仕組みにより、イベントハンドラが登録されたままのオブジェクトはガベージコレクション(GC)の回収対象になりません。

WeakDelegateの設計

通常のDelegateが保持するTargetをWeakReferenceでラップすることで、GCが適切に回収できるようにします。


public class WeakDelegate
{
    private readonly WeakReference _targetReference;
    private readonly MethodInfo _method;

    public WeakDelegate(Delegate @delegate)
    {
        _targetReference = new WeakReference(@delegate.Target);
        _method = @delegate.Method;
    }

    public bool IsAlive => _targetReference.IsAlive;

    public object Target => _targetReference.Target;

    public object Invoke(object[] parameters)
    {
        if (_targetReference.IsAlive)
        {
            return _method.Invoke(_targetReference.Target, parameters);
        }
        return null;
    }
}

イベント発行クラスの実装

イベントハンドラの管理にWeakDelegateを使用するカスタムイベントシステムを構築します。


public class EventPublisher : IDisposable
{
    private readonly Dictionary _handlers = 
        new Dictionary();
    
    public event EventHandler<EventArgs> WeakEvent
    {
        add
        {
            AddHandler("WeakEvent", value);
        }
        remove
        {
            RemoveHandler("WeakEvent", value);
        }
    }

    private void AddHandler(string key, Delegate handler)
    {
        var weakDelegate = new WeakDelegate(handler);
        
        if (!_handlers.TryGetValue(key, out var list))
        {
            list = new List<WeakDelegate>();
            _handlers[key] = list;
        }
        
        list.Add(weakDelegate);
    }

    private void RemoveHandler(string key, Delegate handler)
    {
        if (_handlers.TryGetValue(key, out var list))
        {
            var target = handler.Target;
            var method = handler.Method;
            
            var toRemove = list.Where(wd => 
                wd.Target == target && 
                wd.Method == method).ToList();
            
            foreach (var item in toRemove)
            {
                list.Remove(item);
            }
        }
    }

    public void RaiseEvent()
    {
        InvokeHandlers("WeakEvent");
    }

    private void InvokeHandlers(string key)
    {
        if (_handlers.TryGetValue(key, out var list))
        {
            var toRemove = new List<WeakDelegate>();
            
            foreach (var weakDelegate in list)
            {
                if (weakDelegate.IsAlive)
                {
                    weakDelegate.Invoke(new object[] { this, EventArgs.Empty });
                }
                else
                {
                    toRemove.Add(weakDelegate);
                }
            }
            
            foreach (var item in toRemove)
            {
                list.Remove(item);
            }
        }
    }

    public void Dispose()
    {
        _handlers.Clear();
    }
}

使用例


public class EventSubscriber
{
    public void Subscribe(EventPublisher publisher)
    {
        publisher.WeakEvent += HandleEvent;
    }

    private void HandleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("イベント受信");
    }
}

テストコード


using System;

class Program
{
    static void Main()
    {
        var publisher = new EventPublisher();
        WeakReference subscriberRef = null;

        TestAction(publisher, ref subscriberRef);
        
        // サブスクライバー破棄
        GC.Collect();
        GC.WaitForPendingFinalizers();
        
        // イベント発行(サブスクライバーは既に破棄済み)
        publisher.RaiseEvent();
    }

    static void TestAction(EventPublisher publisher, ref WeakReference subscriberRef)
    {
        var subscriber = new EventSubscriber();
        subscriberRef = new WeakReference(subscriber);
        
        subscriber.Subscribe(publisher);
    }
}

この実装により、サブスクライバーが明示的に破棄されてもイベント発行側が参照を保持し続ける問題を解決できます。

タグ: WeakReference delegate イベント駆動プログラミング メモリ管理 C#

7月4日 17:11 投稿