Pola Event di .NET

EventHandler<TArgs>, penghapusan langganan, dan kebocoran memori.

Daftar isi

Pola event baku di .NET menetapkan satu bentuk yang sama untuk semua event, yaitu tipe delegate EventHandler<TEventArgs> dengan parameter pengirim dan parameter data kejadian. Keseragaman ini membuat pustaka .NET saling menyatu, karena pelanggan cukup mengenal satu pola untuk berlangganan pada tombol, aliran data, maupun kelas penerbit buatan sendiri. Topik ini membahas pembentukan event dengan EventHandler<TEventArgs>, disiplin penghapusan langganan, serta mekanisme kebocoran memori yang muncul ketika langganan ditinggalkan. Setelah topik ini, event dapat dirancang mengikuti konvensi .NET dan masa hidup pelanggannya tetap terkendali.

Pola EventHandler#

Tipe EventHandler<TEventArgs> adalah delegate generik dengan tanda tangan tetap, yaitu metode void yang menerima parameter object? sender dan parameter TEventArgs e. Parameter sender berisi objek penerbit yang memancarkan event, sedangkan parameter e membawa data kejadian dalam sebuah kelas yang diturunkan dari EventArgs. Event bertipe ini dideklarasikan seperti biasa, dan penerbit mengisi kelas argumen sebelum memancarkannya.

Program utuh berikut membangun event SuhuBerubah mengikuti pola tersebut, lengkap dengan kelas argumen khusus. Salin programnya ke berkas Program.cs, lalu jalankan dengan dotnet run --file Program.cs.

csharp Program.cs
// Program pola event baku .NET: EventHandler<TEventArgs> dan langganan.
SuhuSensor sensor = new();

// Metode penanganan ditugaskan ke variabel agar dapat dilepas kembali.
EventHandler<SuhuBerubahEventArgs> tampilkan = (pengirim, argumen) =>
    Console.WriteLine($"Tampilan: suhu {argumen.Nilai} derajat.");

EventHandler<SuhuBerubahEventArgs> catat = (pengirim, argumen) =>
    Console.WriteLine($"Catatan: suhu {argumen.Nilai} direkam.");

sensor.SuhuBerubah += tampilkan;
sensor.SuhuBerubah += catat;

sensor.Ukur(29);

// Langganan tampilkan diakhiri, sedangkan catat tetap aktif.
sensor.SuhuBerubah -= tampilkan;

sensor.Ukur(31);

// Kelas argumen event membawa data yang dikirim penerbit.
class SuhuBerubahEventArgs : EventArgs
{
    public double Nilai { get; init; }
}

// Penerbit yang memancarkan event bertipe EventHandler<TEventArgs>.
class SuhuSensor
{
    public event EventHandler<SuhuBerubahEventArgs>? SuhuBerubah;

    public void Ukur(double suhu)
    {
        SuhuBerubah?.Invoke(this, new SuhuBerubahEventArgs { Nilai = suhu });
    }
}
text Keluaran
Tampilan: suhu 29 derajat.
Catatan: suhu 29 direkam.
Catatan: suhu 31 direkam.

Pemancaran pertama menjalankan kedua penangan, sedangkan pemancaran kedua hanya menjalankan catat, karena langganan tampilkan telah dilepas melalui -= sebelum Ukur dipanggil lagi. Kelas SuhuBerubahEventArgs membawa data kejadian melalui properti Nilai, dan propertinya dideklarasikan init agar data kejadian tidak berubah setelah dikirim. Bentuk ini adalah pola yang digunakan di seluruh .NET, sehingga pelanggan baru dapat menebak bentuk parameternya tanpa membaca isi kelas penerbit.

Menghentikan langganan dengan -=#

Operator -= melepas penangan dari daftar pemanggilan event dengan mencocokkan kesamaan delegate, yaitu kesamaan metode dan objek pemiliknya. Metode bernama yang didaftarkan dua kali menghasilkan dua langganan, dan satu pemanggilan -= hanya melepas satu di antaranya. Ketepatan pencocokan inilah yang menjadikan langganan dapat dikelola satu per satu tanpa mengganggu pelanggan lain.

Lambda anonim menghadapi kendala khusus pada pencocokan ini. Setiap penulisan lambda menciptakan delegate baru yang tidak sama dengan delegate lambda sebelumnya, meskipun badannya identik, sehingga -= dengan lambda baru tidak pernah menemukan langganannya. Solusinya adalah menyimpan lambda ke dalam variabel, sebagaimana tampilkan dan catat pada program di atas, atau menggunakan metode bernama.

Kebocoran memori melalui event#

Kebocoran memori (memory leak) terjadi ketika objek yang seharusnya sudah tidak digunakan tetap tidak dapat diambil oleh garbage collector, yaitu pemulung memori otomatis .NET. Event menjadi jalurnya karena daftar pemanggilan penerbit menyimpan reference ke setiap pelanggan. Selama penerbit hidup dan langganan masih terpasang, penerbit memegang pelanggan itu, dan pemulung memori menilai objek tersebut masih digunakan. Pelanggan tidak pernah bisa diambil meskipun tidak ada bagian lain program yang menggunakannya.

Masalah ini muncul ketika masa hidup penerbit jauh lebih panjang daripada masa hidup pelanggan. Kelas penerbit yang tunggal dan hidup selama aplikasi berjalan, misalnya layanan sensor atau perantara notifikasi, akan mengumpulkan setiap pelanggan yang berlangganan tanpa pernah melepas diri. Aplikasi yang membuka dan menutup halaman atau panel berulang kali menumpuk pelanggan lama di penerbit yang sama, sehingga pemakaian memori naik terus dan pelanggan lama masih menerima kejadian yang seharusnya tidak lagi relevan bagi program.

Cara pencegahannya adalah mengakhiri langganan pada saat yang tepat, biasanya ketika pelanggan selesai digunakan. Kelas pelanggan yang mengimplementasikan IDisposable melepas seluruh langganannya di metode Dispose, sedangkan komponen antarmuka memanfaatkan kejadian siklus hidupnya untuk melepas langganan sebelum dihapus. Untuk penerbit yang usianya sangat panjang, .NET menyediakan pola weak event yang menyimpan pelanggan secara lemah, sehingga pemulung memori tetap dapat mengambil pelanggan tanpa menunggu pelepasan langganan.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika pola event baku dipraktikkan.

  • Langganan dibuat tanpa jalur pelepasan. Pelanggan yang mendaftar dengan lambda tanpa disimpan tidak dapat dilepas secara terarah, sehingga langganannya melekat selama penerbit hidup dan menjadi calon kebocoran memori.
  • -= dianggap mengosongkan seluruh event. Operator itu hanya melepas satu penangan yang cocok, sehingga event dengan banyak pelanggan tetap menyala untuk pelanggan yang tersisa.
  • Kelas argumen dibuat tanpa menurunkan EventArgs. Kode masih dapat berjalan pada .NET modern, tetapi konvensi EventHandler<TEventArgs> mengharapkan turunan EventArgs, dan pelanggaran konvensi membuat pelanggan bingung pada pustaka yang dibagikan.

Ringkasan#

  • Pola event baku .NET menggunakan EventHandler<TEventArgs> dengan parameter pengirim dan kelas argumen turunan EventArgs sebagai pembawa data.
  • Operator -= melepas satu penangan yang cocok, dan lambda anonim hanya dapat dilepas bila delegate-nya disimpan dalam variabel.
  • Penerbit yang hidup lama memegang seluruh pelanggannya melalui daftar pemanggilan, sehingga langganan yang ditinggalkan menjadi kebocoran memori dan penangkalnya adalah pelepasan langganan yang disiplin.

Referensi#