Apakah perlu untuk secara eksplisit menghapus penangan acara di C #

120

Saya memiliki kelas yang menawarkan beberapa acara. Kelas itu dideklarasikan secara global tetapi tidak dijalankan pada deklarasi global itu - itu dibuat sesuai kebutuhan dalam metode yang membutuhkannya.

Setiap kali kelas itu diperlukan dalam sebuah metode, itu adalah instance dan penangan kejadian didaftarkan. Apakah perlu untuk menghapus penangan kejadian secara eksplisit sebelum metode keluar dari ruang lingkup?

Ketika metode keluar dari ruang lingkup, begitu pula instance kelas. Apakah meninggalkan penangan kejadian yang terdaftar dengan contoh yang keluar dari ruang lingkup memiliki implikasi jejak memori? (Saya ingin tahu apakah pengendali event mencegah GC melihat instance kelas tidak lagi direferensikan.)

rp.
sumber

Jawaban:

184

Dalam kasus Anda, semuanya baik-baik saja. Itu adalah objek yang menerbitkan acara yang membuat target dari penangan acara tetap hidup. Jadi jika saya memiliki:

publisher.SomeEvent += target.DoSomething;

kemudian publishermemiliki referensi ke targettapi tidak sebaliknya.

Dalam kasus Anda, penerbit akan memenuhi syarat untuk pengumpulan sampah (dengan asumsi tidak ada referensi lain untuk itu) sehingga fakta bahwa itu mendapat referensi ke target pengendali kejadian tidak relevan.

Kasus rumitnya adalah ketika penerbit berumur panjang tetapi pelanggan tidak ingin menjadi - dalam hal ini Anda perlu berhenti berlangganan penangan. Misalnya, Anda memiliki beberapa layanan transfer data yang memungkinkan Anda berlangganan pemberitahuan asinkron tentang perubahan bandwidth, dan objek layanan transfer berumur panjang. Jika kita melakukan ini:

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;

(Anda sebenarnya ingin menggunakan blok terakhir untuk memastikan Anda tidak membocorkan penangan peristiwa.) Jika kami tidak berhenti berlangganan, maka BandwidthUIakan berlaku setidaknya selama layanan transfer.

Secara pribadi saya jarang menemukan ini - biasanya jika saya berlangganan ke suatu acara, target acara itu hidup setidaknya selama penerbit - formulir akan bertahan selama tombol yang ada di atasnya, misalnya. Penting untuk mengetahui tentang potensi masalah ini, tetapi saya pikir beberapa orang khawatir tentang itu ketika mereka tidak membutuhkannya, karena mereka tidak tahu ke arah mana referensi tersebut mengarah.

EDIT: Ini untuk menjawab komentar Jonathan Dickinson. Pertama, lihat dokumen untuk Delegate.Equals (objek) yang secara jelas memberikan perilaku kesetaraan.

Kedua, berikut adalah program singkat tapi lengkap untuk menunjukkan cara berhenti berlangganan:

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();
    }
}

Hasil:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Diuji di Mono dan .NET 3.5SP1.)

Edit lebih lanjut:

Hal ini untuk membuktikan bahwa event publisher bisa dihimpun selagi masih ada referensi ke subscriber.

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);
    }
}

Hasil (dalam .NET 3.5SP1; Mono tampaknya berperilaku agak aneh di sini. Akan memeriksanya beberapa saat):

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
Jon Skeet
sumber
2
Saya setuju dengan ini, tetapi jika memungkinkan, dapatkah Anda menjelaskan secara singkat atau lebih disukai merujuk pada contoh dari apa yang Anda maksud dengan "tetapi pelanggan tidak ingin menjadi"?
Peter McG
@ Jon: Sangat dihargai, itu tidak umum tetapi seperti yang Anda katakan saya telah melihat orang-orang mengkhawatirkan hal ini secara tidak perlu.
Peter McG
- = Tidak bekerja. - = Akan menghasilkan delegasi baru, dan delegasi tidak memeriksa kesetaraan dengan menggunakan metode target, mereka melakukan objek.ReferenceEquals () pada delegasi. Delegasi baru tidak ada dalam daftar: tidak berpengaruh (dan anehnya tidak melontarkan kesalahan).
Jonathan C Dickinson
2
@Jonathan: Tidak, delegasi memeriksa kesetaraan menggunakan metode target. Akan terbukti dalam sunting.
Jon Skeet
Saya mengakui. Saya bingung dengan delegasi anonim.
Jonathan C Dickinson
8

Dalam kasus Anda, Anda baik-baik saja. Saya awalnya membaca pertanyaan Anda secara terbalik, bahwa pelanggan berada di luar cakupan, bukan penerbit . Jika penerbit acara keluar dari ruang lingkup, maka referensi ke pelanggan (bukan pelanggan itu sendiri, tentu saja!) Ikut serta dan tidak perlu menghapusnya secara eksplisit.

Jawaban asli saya ada di bawah, tentang apa yang terjadi jika Anda membuat pelanggan acara dan membiarkannya keluar dari ruang lingkup tanpa berhenti berlangganan. Ini tidak berlaku untuk pertanyaan Anda tetapi saya akan membiarkannya di tempat untuk sejarah.

Jika kelas masih terdaftar melalui penangan acara, maka kelas masih bisa dijangkau. Itu masih benda hidup. GC yang mengikuti grafik peristiwa akan menemukannya terhubung. Ya, Anda pasti ingin menghapus event handler secara eksplisit.

Hanya karena objek di luar ruang lingkup alokasi aslinya tidak berarti itu adalah calon GC. Selama referensi langsung tetap ada, itu hidup.

Eddie
sumber
1
Saya tidak percaya pembatalan langganan apa pun diperlukan di sini - GC melihat referensi dari penerbit acara, bukan dari penerbit itu, dan penerbit yang kami khawatirkan di sini.
Jon Skeet
@ Jon Skeet: Anda benar. Saya membaca pertanyaan itu dari belakang. Saya telah mengoreksi jawaban saya untuk mencerminkan kenyataan.
Eddie