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


120

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

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

メソッドがスコープから外れると、クラスのインスタンスも外れます。スコープ外になるそのインスタンスに登録されたイベントハンドラーを残すと、メモリフットプリントに影響がありますか?(イベントハンドラーが、GCがクラスインスタンスを参照されていないものと見なさないようにしているのかと思います。)

回答:


184

あなたの場合、すべてが大丈夫です。イベントハンドラーのターゲットをライブに保つのは、イベントを公開するオブジェクトです。だから私が持っている場合:

publisher.SomeEvent += target.DoSomething;

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

あなたのケースでは、パブリッシャーはガベージコレクションの対象になります(他に参照がない場合)、イベントハンドラーのターゲットへの参照があることは関係ありません。

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

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ライブを解除しなかった場合は、少なくとも転送サービスと同じくらい存続します。

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

編集:これはジョナサンディキンソンのコメントに答えることです。まず、明らかに等価性の振る舞いを与える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

2
私はこれに同意しますが、可能であれば、「しかし、加入者はなりたくない」という意味の例を簡潔に説明したり、できれば参照したりできますか?
Peter McG、

@ジョン:ありがたいことに、それは一般的ではありませんが、あなたが言うように、人々がこれについて不必要に心配するのを見てきました。
Peter McG、

-=機能しません。-=結果として新しいデリゲートが作成され、デリゲートはターゲットメソッドを使用して等価性をチェックせず、デリゲートに対してobject.ReferenceEquals()を実行します。新しいデリゲートはリストに存在しません。影響はありません(奇妙なことにエラーをスローしません)。
ジョナサンCディキンソン

2
@ジョナサン:いいえ、デリゲートはターゲットメソッドを使用して同等性をチェックします。編集で証明されます。
Jon Skeet、

私は認めます。匿名のデリゲートと混乱しました。
ジョナサンCディキンソン

8

あなたの場合、あなたは大丈夫です。私は元々あなたの質問を逆に読みました、サブスクライバーパブリッシャーではなく範囲外に出ていました。イベントパブリッシャーが範囲外になる場合、サブスクライバーへの参照(もちろん、サブスクライバー自体ではありません)はそれに伴っており、明示的に削除する必要はありません。

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

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

オブジェクトが元の割り当ての範囲外であっても、GCの候補であるとは限りません。ライブ参照が残っている限り、ライブ参照です。


1
ここではサブスクリプション解除が必要だとは思いません。GCはイベントパブリッシャーからの参照はなく、イベントパブリッシャーからの参照を参照ます。ここで私たちが懸念しているのはパブリッシャーです。
Jon Skeet、

@ジョン・スキート:その通りです。質問を逆に読みました。現実を反映するように答えを修正しました。
Eddie
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.