イベントリスナが先にお亡くなりになっています。あれ?残念ながらメモリリークです。あのね、WeakEventパターンを使うといいと思うんだ。WeakEventManagerから派生して、抽象的なWeakEventTriggeredManagerクラスを作ってみよう。
WeakEvent パターン(msdn.microsoft.com)というのがあります。
WPFでごにょごにょしているような方であれば、ご存知の人も多いようですが、
WPFと無縁の人たちにとっては、認知度がかなり低いようです。
WeakEventパターン*1が有用となるシナリオは、なにもWPFアプリケーションに限った話ではないので、
.NET Framework3.0以降が利用できる環境であれば、ぜひともお勉強して利用を検討するとよいと思う。
で、MSDNに書かれている次のことが、ちょっと引っかかりますね。
WeakEventManager
通常、マネージャ クラスは、パターンを実装するイベントと 1 対 1 の関係で作成します。
たとえば、1 つのイベント Spin がある場合、このイベント専用の弱いイベント マネージャとして
1 つの SpinEventManager クラスを派生させます。このイベントが複数のソースのクラスにあり、
一般に各クラスで同様に動作し、イベント データ型を共有する場合は、それぞれ同じマネージャを使用することができます。
確かに、WeakEventManagerクラスから派生して1つずつ丁寧に作ってあげれば、WeakEventパターンは実現できます。
でも、対象とするイベントソースやイベントリスナが異なるごとに、
都度マネージャクラスを作るというのは面倒くさいです。超面倒くさすぎますし、クラス爆発してしまいますね。
なので、ここはジェネリックを活用してある程度汎用的に利用できるように
抽象化したWeakEventTriggeredManager
EventHandler{TEventArgs}を持っていることを表すインターフェイス
EventHandler
対象となるイベントソースが必要に応じて実装することになります。
using System; namespace ConsoleApplication1 { public interface IHaveEventHandler<TEventArgs> where TEventArgs : EventArgs { event EventHandler<TEventArgs> EventTriggered; } }
IWeakEventListenerを拡張する
IWeakEventListener インターフェイス(msdn.microsoft.com)を拡張して、
対象となるイベントリスナがイベントソースのことを知ることができるようにします。
using System; using System.Windows; namespace ConsoleApplication1 { public interface IWeakEventListenerEx<TEventArgs> : IWeakEventListener where TEventArgs : EventArgs { IHaveEventHandler<TEventArgs> EventSource { get; } } }
WeakEventTriggeredManager{TSource, TEventArgs}クラス
今回のお話のメインとなるクラスです。
WeakEventManagerから派生して、WeakEventのトリガーを管理する抽象的なクラスを作ります。
IHaveEventHandler
イベントデータが格納されているTEventArgsについての弱参照イベントのトリガーを管理します。
基底クラスのProtectedAddListenerメソッドおよびProtectedRemoveListenerメソッドは、イベントソースごとに
単一の内部WeakEventManager.ListenerList にイベントリスナを追加します。
イベントとソースの組み合わせごとに複数のリスナリストを保持する必要がある場合は、
基底クラスのメソッドを使用せずに、独自の WeakEventManager.ListenerListインスタンスを作成して、
AddListenerで適切なリストにイベントリスナを追加し、DeliverEventではなくDeliverEventToListを呼び出して
適切なリスナリストにイベントを配信する必要があります。
今回はその必要がないので、基底クラスのメソッドにおまかせする形で実装しています。
using System; using System.Windows; using System.Diagnostics; namespace ConsoleApplication1 { public sealed class WeakEventTriggeredManager<TSource, TEventArgs> : WeakEventManager where TSource : class, IHaveEventHandler<TEventArgs> where TEventArgs : EventArgs { protected override void StartListening(object source) { var src = source as TSource; Debug.Assert(src != null); src.EventTriggered += new EventHandler<TEventArgs>(OnEventTriggered); #if DEBUG Console.WriteLine("AddHandler - [{0}]", typeof(TEventArgs).ToString()); #endif } protected override void StopListening(object source) { var src = source as TSource; Debug.Assert(src != null); src.EventTriggered -= new EventHandler<TEventArgs>(OnEventTriggered); #if DEBUG Console.WriteLine("RemoveHandler - [{0}]",typeof(TEventArgs).ToString()); #endif } public static void AddListener(IWeakEventListenerEx<TEventArgs> listener) { WeakEventTriggeredManager<TSource, TEventArgs>.CurrentManager.ProtectedAddListener(listener.EventSource, listener); } public static void RemoveListener(IWeakEventListenerEx<TEventArgs> listener) { WeakEventTriggeredManager<TSource, TEventArgs>.CurrentManager.ProtectedRemoveListener(listener.EventSource, listener); } public static void RemoveNullReferenceListener(IHaveEventHandler<TEventArgs> source) { WeakEventTriggeredManager<TSource, TEventArgs>.CurrentManager.StopListening(source); } private void OnEventTriggered(object sender, EventArgs e) { base.DeliverEvent(sender, e); } private static volatile WeakEventTriggeredManager<TSource, TEventArgs> _manager = null; private static WeakEventTriggeredManager<TSource, TEventArgs> CurrentManager { get { if (_manager != null) return _manager; Type type = typeof(WeakEventTriggeredManager<TSource, TEventArgs>); _manager = new WeakEventTriggeredManager<TSource, TEventArgs>(); WeakEventManager.SetCurrentManager(type, _manager); return _manager; } } } }
AbstractWeakEventListenerクラス
WeakEventパターンで利用する抽象的なイベントリスナを定義しておくと、毎度々々似たような実装をすることを減らすことができて便利です。
拡張したIWeakEventListenerEx
ReceiveWeakEventメソッドは、具象クラスごとに異なりますので仮想関数にしておきます。
IDisposableインターフェイスを実装しているのは、イベントリスナがDisposeされたタイミングで
イベントリスナがWeakEventManagerから削除されるようにしたいからです。
また、イベントリスナが参照されなくなってデストラクタが走ったときにも、
イベントソースからイベントリスナのハンドルを解除したいのでデストラクタもそのように実装します。
using System; using System.Diagnostics; namespace ConsoleApplication1 { public abstract class AbstractWeakEventListener<TSource, TEventArgs> : IWeakEventListenerEx<TEventArgs>, IDisposable where TSource : class, IHaveEventHandler<TEventArgs> where TEventArgs : EventArgs { public AbstractWeakEventListener(TSource source) { Debug.Assert(source != null); _disposed = false; EventSource = source; } ~AbstractWeakEventListener() { this.Dispose(false); WeakEventTriggeredManager<TSource, TEventArgs>.RemoveNullReferenceListener(EventSource); #if DEBUG Console.WriteLine("デストラクタに来たよ"); #endif } private bool _disposed; protected virtual void Dispose(bool disposing) { lock (this) { if (this._disposed) return; this._disposed = true; if (disposing) WeakEventTriggeredManager<TSource, TEventArgs>.RemoveListener(this); } } #region IWeakEventListener メンバ public abstract bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e); #endregion #region IDisposable メンバ public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } #endregion #region IWeakEventListenerEx<TEventArgs> メンバ private IHaveEventHandler<TEventArgs> _eventSource = null; public IHaveEventHandler<TEventArgs> EventSource { get { return _eventSource; } private set { _eventSource = value; } } #endregion } }
イベントソースの実装サンプル
イベントソースが持つイベントハンドラのイベントデータ種類ごとに
各種インターフェイスを明示的に実装します。
using System; using System.ComponentModel; namespace ConsoleApplication1 { public class SampleEventSource : IHaveEventHandler<EventArgs>, IHaveEventHandler<CancelEventArgs> { public void Raise() { _event(this, new EventArgs()); _cancelEvent(this, new CancelEventArgs()); } #region IHaveEventHandler<EventArgs> メンバ event EventHandler<EventArgs> _event = (sender, e) => { }; event EventHandler<EventArgs> IHaveEventHandler<EventArgs>.EventTriggered { add { lock (_event) { _event += value; } } remove { lock (_event) { _event -= value; } } } #endregion #region IHaveEventHandler<CancelEventArgs> メンバ event EventHandler<CancelEventArgs> _cancelEvent = (sender, e) => { }; event EventHandler<CancelEventArgs> IHaveEventHandler<CancelEventArgs>.EventTriggered { add { lock (_cancelEvent) { _cancelEvent += value; } } remove { lock (_cancelEvent) { _cancelEvent -= value; } } } #endregion } }
イベントリスナの実装サンプル
AbstractWeakEventListener
ReceiveWeakEventでは、イベントを発砲する振る舞いを決めます。
サンプルその1
using System; namespace ConsoleApplication1 { public sealed class HogeEventListener<TSource, TEventArgs> : AbstractWeakEventListener<TSource, TEventArgs> where TSource : class, IHaveEventHandler<TEventArgs> where TEventArgs : EventArgs { public HogeEventListener(TSource source) :base(source) {} private void source_EventTriggered1(object sender, TEventArgs e) { Console.WriteLine("Hoge::EventTriggered1が発砲されたよ"); } private void source_EventTriggered2(object sender, TEventArgs e) { Console.WriteLine("Hoge::EventTriggered2が発砲されたよ"); } private void source_EventTriggered3(object sender, TEventArgs e) { Console.WriteLine("Hoge::EventTriggered3が発砲されたよ"); } public override bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(WeakEventTriggeredManager<TSource, TEventArgs>)) { source_EventTriggered3(sender, (TEventArgs)e); //source_EventTriggered2(sender, (TEventArgs)e); source_EventTriggered1(sender, (TEventArgs)e); return true; } return false; } } }
サンプルその2
using System; namespace ConsoleApplication1 { public sealed class PiyoEventListener<TSource, TEventArgs> : AbstractWeakEventListener<TSource, TEventArgs> where TSource : class, IHaveEventHandler<TEventArgs> where TEventArgs : EventArgs { public PiyoEventListener(TSource source) :base(source) {} private void source_EventTriggered1(object sender, TEventArgs e) { Console.WriteLine("Piyo::EventTriggered1が発砲されたよ"); } private void source_EventTriggered2(object sender, TEventArgs e) { Console.WriteLine("Piyo::EventTriggered2が発砲されたよ"); } private void source_EventTriggered3(object sender, TEventArgs e) { Console.WriteLine("Piyo::EventTriggered3が発砲されたよ"); } public override bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(WeakEventTriggeredManager<TSource, TEventArgs>)) { source_EventTriggered1(sender, (TEventArgs)e); source_EventTriggered2(sender, (TEventArgs)e); //source_EventTriggered3(sender, (TEventArgs)e); return true; } return false; } } }
使ってみる
このサンプルでWeakEventパターンの動作と有用性を確認できると思います。
using System; using System.ComponentModel; using System.Diagnostics; namespace ConsoleApplication1 { class Program { static void Main() { Example1(); Console.WriteLine(); Example2(); Console.WriteLine(); Example3(); Console.ReadKey(); } [Conditional("DEBUG")] static void Example1() { Console.WriteLine("*Example1*"); var source = new SampleEventSource(); var hoge = new HogeEventListener<SampleEventSource, EventArgs>(source); var piyo = new PiyoEventListener<SampleEventSource, CancelEventArgs>(source); //リスナを追加 WeakEventTriggeredManager<SampleEventSource, EventArgs>.AddListener(hoge); WeakEventTriggeredManager<SampleEventSource, CancelEventArgs>.AddListener(piyo); source.Raise(); //hogeリスナには、Disposeせずに強制的にガベコレしてリスナにお亡くなりになって頂く hoge = null; GC.Collect(); GC.WaitForPendingFinalizers(); //piyoリスナは生きているので発砲される source.Raise(); } [Conditional("DEBUG")] static void Example2() { Console.WriteLine("*Example2*"); var source = new SampleEventSource(); using (var hoge = new HogeEventListener<SampleEventSource, EventArgs>(source)) using (var piyo = new PiyoEventListener<SampleEventSource, CancelEventArgs>(source)) { //リスナを追加 WeakEventTriggeredManager<SampleEventSource, EventArgs>.AddListener(hoge); WeakEventTriggeredManager<SampleEventSource, CancelEventArgs>.AddListener(piyo); source.Raise(); } //いずれのリスナもお亡くなりになっているので発砲されない source.Raise(); } [Conditional("DEBUG")] static void Example3() { Console.WriteLine("*Example3*"); var source = new SampleEventSource(); using (var piyo = new PiyoEventListener<SampleEventSource, CancelEventArgs>(source)) { var hoge = new HogeEventListener<SampleEventSource, EventArgs>(source); //リスナを追加 WeakEventTriggeredManager<SampleEventSource, EventArgs>.AddListener(hoge); WeakEventTriggeredManager<SampleEventSource, CancelEventArgs>.AddListener(piyo); source.Raise(); //リスナを明示的に削除してもかまわない WeakEventTriggeredManager<SampleEventSource, CancelEventArgs>.RemoveListener(piyo); } //hogeリスナは生きているので発砲される source.Raise(); } } }
まとめ WeakEventパターンのうんちく
C#でのイベント割り当てをする場合の一般的な書き方のひとつは、下記のとおりです。
source.SomeEvent += new SomeEventHandler(MyEventHandler);
この手法では、イベントソースからイベントリスナのイベントハンドラへ強い参照がされます。
イベントソースがインスタンスメソッドのイベントハンドラへ強参照を保持している場合、
対象とするイベントリスナへの参照を保持します。イベントハンドラへの参照が維持されている限り、
イベントリスナへの参照も維持されることになります。
イベントリスナをDisposeしたとき、一見イベントリスナへの参照がすべて失われたように見えても、
イベントソースが存命である限りは、「イベントソースからイベントハンドラへの参照」が残っているため、
イベントハンドラからイベントリスナへの参照も残り続けます。参照が残っているということは、
オブジェクトインスタンスは存命するので、結果メモリリークに繋がってしまうというわけです。
この問題を解決するには、イベントソースに接続されたイベントハンドラを適切なタイミングでRemoveする必要があります。
最も考えられるタイミングのひとつに、イベントリスナがDisposeされたときというのがあります。
ただし、イベントソースに接続されたイベントハンドラが削除されるタイミングがDisposeされたときであるかどうかは、
シナリオやコンテキストによって異なる場合があります。イベントリスナは、イベント接続が不要になった時点で
イベントハンドラの接続を解除するのが適切であると言えます。
それを容易に且つ適切に実現できるのがWeakEventパターンというわけです。
WeakEventパターンでは、イベントリスナのオブジェクトの有効期間の特性にまったく影響を与えることなく、
イベントリスナをイベントに登録し、適切なイベントを受け取ることができるよう設計されています。
この参照は弱い参照であり、このことから WeakEventパターンおよび関連する各種APIの名前が付けられています。
弱い参照は、オブジェクトがガベージコレクションによるクリアの対象になっている状態のままで、
そのオブジェクトにアクセスすることができます。
なので必要であればイベントリスナへの参照は存続し、そうでなければ破棄されます。
破棄されたオブジェクトへの収集不能なハンドラ参照を継続することもないというわけです。
開発者が、すべてのシナリオにおいてイベント割り当てを適切なタイミングで削除をしないと、
メモリーリークが発生してしまう恐れがあるということを把握しつつ対処することは難しいので(バグの温床となる)、
クラス間でイベント割り当てを行うようなシナリオでは、
WeakEventパターンを利用することが望ましいというわけです。というか使ってくださいお願いします。
余談的なぼやき
目くそ鼻くそのメモリリークが発生していたところで、気付かずに(気付かないふりをして)放置されているシステムってのが世の中にはたくさんある。
これも昨今の開発者(自分含む)が富豪的プログラミングに慣れすぎてしまったことの代償でしょうか。
まぁ確かに現在の富豪ハードウェアの下では、目くそ鼻くそのメモリリークが
大きな問題として浮かび上がってくるようなケースは少ないんだろうけど。でもねえ。
*1:パターンと言うよりもイディオム的な側面が強いような気もする