web-dev-qa-db-ja.com

C#でイベントハンドラを明示的に削除する必要がありますか

いくつかのイベントを提供するクラスがあります。そのクラスはグローバルに宣言されますが、そのグローバル宣言ではインスタンス化されません。それは、それを必要とするメソッドで必要に応じてインスタンス化されます。

メソッドでそのクラスが必要になるたびに、インスタンス化され、イベントハンドラーが登録されます。メソッドがスコープ外になる前に、イベントハンドラーを明示的に削除する必要がありますか?

メソッドが範囲外になると、クラスのインスタンスも範囲外になります。スコープ外に出ているインスタンスに登録されたイベントハンドラーを残すことは、メモリフットプリントに影響しますか? (イベントハンドラーが、GCがクラスインスタンスを参照されていないものとして認識しないようにするのかどうか疑問に思っています。)

113
rp.

あなたの場合、すべてが正常です。イベントハンドラーのtargetsを保持するイベントをpublishedするオブジェクトです。だから私が持っている場合:

publisher.SomeEvent += target.DoSomething;

publisherにはtargetへの参照がありますが、その逆はありません。

あなたの場合、パブリッシャーはガベージコレクションの対象になります(他に参照がなければ)、イベントハンドラーターゲットへの参照を取得するという事実は無関係です。

トリッキーなケースは、パブリッシャーの寿命は長いが、サブスクライバーが望んでいない場合です。thatの場合、ハンドラーのサブスクリプションを解除する必要があります。たとえば、帯域幅の変更に関する非同期通知をサブスクライブできるデータ転送サービスがあり、転送サービスオブジェクトの寿命が長いとします。これを行う場合:

BandwidthUI ui = new BandwidthUI();
transferService.BandwidthChanged += ui.HandleBandwidthChange;
// Suppose this blocks until the transfer is complete
transferService.Transfer(source, destination);
// We now have to unsusbcribe from the event
transferService.BandwidthChanged -= ui.HandleBandwidthChange;

(実際には、finallyブロックを使用して、イベントハンドラーがリークしないようにする必要があります。)サブスクリプションを解除しなかった場合、BandwidthUIは少なくとも転送サービスが存在する限り存続します。

個人的に私はこれに出くわすことはめったにありません-通常、イベントにサブスクライブする場合、そのイベントのターゲットは少なくともパブリッシャーと同じ長さで存続します-たとえば、フォームはボタンが置かれている限り続きます。この潜在的な問題について知っておく価値はありますが、参照がどの方向に進むのかわからないため、必要なときに心配する人もいると思います。

EDIT:これは、ジョナサン・ディキンソンのコメントに答えるためです。まず、 Delegate.Equals(object) のドキュメントを見てください。これにより、平等な振る舞いが明確になります。

次に、サブスクリプションが機能していないことを示す短いが完全なプログラムを次に示します。

using System;

public class Publisher
{
    public event EventHandler Foo;

    public void RaiseFoo()
    {
        Console.WriteLine("Raising Foo");
        EventHandler handler = Foo;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        else
        {
            Console.WriteLine("No handlers");
        }
    }
}

public class Subscriber
{
    public void FooHandler(object sender, EventArgs e)
    {
        Console.WriteLine("Subscriber.FooHandler()");
    }
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;
         publisher.RaiseFoo();
         publisher.Foo -= subscriber.FooHandler;
         publisher.RaiseFoo();
    }
}

結果:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Monoおよび.NET 3.5SP1でテスト済み。)

さらに編集:

これは、サブスクライバーへの参照がまだある間にイベント発行元を収集できることを証明するためです。

using System;

public class Publisher
{
    ~Publisher()
    {
        Console.WriteLine("~Publisher");
        Console.WriteLine("Foo==null ? {0}", Foo == null);
    }

    public event EventHandler Foo;
}

public class Subscriber
{
    ~Subscriber()
    {
        Console.WriteLine("~Subscriber");
    }

    public void FooHandler(object sender, EventArgs e) {}
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;

         Console.WriteLine("No more refs to publisher, "
             + "but subscriber is alive");
         GC.Collect();
         GC.WaitForPendingFinalizers();         

         Console.WriteLine("End of Main method. Subscriber is about to "
             + "become eligible for collection");
         GC.KeepAlive(subscriber);
    }
}

結果(.NET 3.5SP1の場合、Monoはここで少し奇妙に動作するように見えます。それについてはしばらく調べます):

No more refs to publisher, but subscriber is alive
~Publisher
Foo==null ? False
End of Main method. Subscriber is about to become eligible for collection
~Subscriber
176
Jon Skeet

あなたの場合、あなたは大丈夫です。 subscriberpublisherではなく、範囲外になっているという質問を元々読みまし​​た。イベント発行元が範囲外になった場合、サブスクライバへのreferences(もちろんサブスクライバ自体ではありません!)が一緒に移動し、明示的に削除する必要はありません。

私の元の答えは、イベントsubscriberを作成し、サブスクライブを解除せずに範囲外にした場合に何が起こるかについてです。それはあなたの質問には当てはまりませんが、歴史のために残しておきます。

クラスがイベントハンドラーを介してまだ登録されている場合は、引き続き到達可能です。まだライブオブジェクトです。イベントグラフに続くGCは、接続されていることを検出します。はい、イベントハンドラーを明示的に削除する必要があります。

オブジェクトが元の割り当ての範囲外にあるからといって、それがGCの候補であることを意味するわけではありません。ライブ参照が残っている限り、ライブ参照です。

7
Eddie