これらのサブスクライブされていないイベントのメモリリークはいつ発生しますか?イベントのサブスクライブを解除するには、デストラクタを作成するか、IDisposableを実装する必要がありますか?
2 に答える
AがBを参照しているとしましょう。さらに、Bの処理は完了したと考えており、ガベージ コレクションが行われることを期待しているとします。
ここで、Aが到達可能[1] である場合、Bは「処理が完了している」という事実にもかかわらず、ガベージ コレクションされません。これは、本質的に、メモリリークです [2]
BがAのイベントをサブスクライブする場合、同じ状況になります。Aは、イベント ハンドラー デリゲートを介してBへの参照を持っています。
では、いつ問題が発生するのでしょうか。上記のように、参照オブジェクトが到達可能な場合のみ。この場合、Fooインスタンスが使用されなくなったときにリークが発生する可能性があります。
class Foo
{
Bar _bar;
public Foo(Bar bar)
{
_bar = bar;
_bar.Changed += BarChanged;
}
void BarChanged(object sender, EventArgs e) { }
}
リークが発生する理由は、コンストラクターで渡されたBarインスタンスが、それを使用するFooインスタンスよりも長い寿命を持つ可能性があるためです。サブスクライブされたイベント ハンドラーは、Fooを存続させることができます。
この場合、メモリ リークが発生しないように、イベントからサブスクライブを解除する方法を提供する必要があります。これを行う 1 つの方法は、 FooにIDisposableを実装させることです。その利点は、完了時にDispose()を呼び出す必要があることをクラス コンシューマーに明確に通知することです。もう 1 つの方法は、Subscribe( ) メソッドとUnsubscribe()メソッドを別々に持つことですが、これでは型の期待を伝えません。これらはオプションであり、一時的な結合を呼び出して導入することはできません。
私の推奨事項は次のとおりです。
class sealed Foo : IDisposable
{
readonly Bar _bar;
bool _disposed;
...
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_bar.Changed -= BarChanged;
}
}
...
}
または、次のようにします。
class sealed Foo : IDisposable
{
Bar _bar;
...
public void Dispose()
{
if (_bar != null)
{
_bar.Changed -= BarChanged;
_bar = null;
}
}
...
}
一方、参照元のオブジェクトに到達できない場合、リークは発生しません。
class sealed Foo
{
Bar _bar;
public Foo()
{
_bar = new Bar();
_bar.Changed += BarChanged;
}
void BarChanged(object sender, EventArgs e) { }
}
この場合、すべてのFooインスタンスは常に、構成されたBarインスタンスよりも長く存続します。Fooに到達できない場合、そのBarにも到達できます。サブスクライブされたイベント ハンドラーは、ここでFooを存続させることができません。これの欠点は、Barが単体テストのシナリオでモック化する必要がある依存関係である場合、コンシューマーによって (きれいな方法で) 明示的にインスタンス化することはできず、注入する必要があることです。
イベント ハンドラーには、(デリゲートのTarget
プロパティで) ハンドラーを宣言するオブジェクトへの強い参照が含まれています。
イベント ハンドラーが削除されるまで (または、イベントを所有するオブジェクトが参照されなくなるまで)、ハンドラーを含むオブジェクトは収集されません。
ハンドラーが不要になったら (おそらく でDispose()
) ハンドラーを削除することで、これを修正できます。
ファイナライザーは収集できるようになった後にのみ実行されるため、ファイナライザーは役に立ちません。