Menu

Event C#: Keyword event, EventHandler, Subscribe, dan Raise

Cara kerja event di C#: mendeklarasikan event dengan EventHandler dan EventHandler<T>, class EventArgs buatan sendiri, subscribe dan unsubscribe dengan += dan -=, memunculkan event dengan aman lewat ?.Invoke, kenapa event lebih baik daripada field delegate public, dan memory leak karena lupa unsubscribe.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Event memungkinkan sebuah class mengumumkan bahwa sesuatu terjadi tanpa tahu siapa yang mendengarkan. Object lain melakukan subscribe dengan +=, dan saat class itu memunculkan event-nya, method milik setiap subscriber dijalankan.

Output:

Order A-17 saved
  receipt emailed for A-17
Order A-18 saved
  receipt emailed for A-18
  free gift added to A-18

Shop tidak tahu bahwa struk atau hadiah itu ada. Class ini memunculkan OrderPlaced, dan apa pun yang sudah subscribe akan bereaksi. Menambahkan reaksi baru (poin loyalitas, analitik) berarti menambahkan subscriber, bukan mengubah PlaceOrder.

Mendeklarasikan event

Deklarasi event adalah member bertipe delegate dengan keyword event:

public event EventHandler<OrderPlacedEventArgs> OrderPlaced;

Tipe delegate-nya menentukan signature handler. Kode .NET hampir selalu memakai salah satu dari dua tipe bawaan, dan mengikuti konvensi ini membuat event-mu terasa familier bagi setiap developer .NET lain:

  • EventHandler: void (object sender, EventArgs e), untuk event yang tidak membawa data. Munculkan dengan EventArgs.Empty.
  • EventHandler<TEventArgs>: void (object sender, TEventArgs e), dengan TEventArgs adalah class milikmu yang membawa datanya.

Konvensi di sekitarnya:

  • sender adalah object yang memunculkan event, biasanya this.
  • Class datanya diberi nama {Something}EventArgs, diturunkan dari EventArgs (opsional sejak .NET 4.5, tetapi masih lazim), dan mengekspos property read-only.
  • Event diberi nama sesuai apa yang terjadi: OrderPlaced, Closed, PriceChanged. Nama berakhiran ...ing (Closing) dipakai untuk event yang dimunculkan sebelum aksinya, sering kali dengan cara untuk membatalkannya.
  • Di class yang dimaksudkan untuk diwarisi, event dimunculkan lewat method protected virtual void OnOrderPlaced(OrderPlacedEventArgs e), supaya class turunan bisa ikut campur.

Subscribe dan unsubscribe

+= menambahkan handler dan -= menghapusnya. Handler bisa berupa method atau lambda. Untuk menghapus lambda nanti, simpan lambda itu di variabel, karena menulis lambda yang sama lagi menghasilkan delegate berbeda yang tidak akan ditemukan oleh -=:

Output:

set to 32
  display shows 32 C
  alarm: too hot
set to 35
  display shows 35 C
set to 20
  display shows 20 C

Menghapus handler yang tidak pernah ditambahkan bukan error; hal itu tidak berbuat apa-apa. EventHandler<int> menunjukkan bahwa argumen tipenya tidak harus diturunkan dari EventArgs di .NET modern, meskipun class khusus memberi ruang untuk menambahkan field nanti tanpa merusak subscriber.

Handler berjalan secara sinkron, sesuai urutan subscribe, di thread yang memunculkan event. Kalau satu handler melempar exception, handler sisanya tidak dijalankan dan exception-nya diteruskan ke kode yang memunculkan event.

Memunculkan event dengan aman

Event tanpa subscriber berisi null. Memanggilnya secara langsung akan melempar NullReferenceException, jadi munculkan dengan operator null-conditional:

OrderPlaced?.Invoke(this, args);

Sebelum C# 6, polanya adalah menyalin field ke variabel lokal, mengeceknya, lalu memanggil salinannya. Salinan itu penting di kode multithread: mengecek OrderPlaced != null lalu memanggil OrderPlaced(...) membaca field-nya dua kali, dan thread lain bisa menghapus handler terakhir di antaranya. ?. membacanya sekali, jadi lebih singkat sekaligus benar.

Kenapa event, bukan field delegate public

Tanpa keyword event, field delegate public tetap bisa dipakai untuk subscribe, tetapi memberi setiap subscriber kendali penuh:

Output:

analytics ping
analytics ping

Satu = yang diketik alih-alih += diam-diam menghapus dua handler sebelumnya, dan kode dari luar bisa memunculkan "klik" tanpa ada klik sama sekali. Menandai field-nya dengan event membuat kedua baris itu menjadi error compile:

error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')

Di dalam Button, event itu tetap berperilaku seperti field delegate biasa, jadi class itu bisa memanggilnya dan mengecek apakah nilainya null.

Leak karena lupa unsubscribe

Saat kamu subscribe, event menyimpan sebuah delegate, dan delegate itu menyimpan reference ke object subscriber. Selama publisher masih hidup dan handler-nya masih terpasang, subscriber tidak bisa dibersihkan garbage collector, dan terus menerima event setelah kamu selesai memakainya:

Output:

  closed widget shows 101.5
  open widget shows 101.5
subscribers left: 1
  closed widget shows 99.0

Mengisi leaky dengan null tidak berpengaruh apa-apa bagi feed: daftar handler-nya masih mereferensikan widget itu, jadi widget yang "ditutup" terus diperbarui dan tetap ada di memori. Widget di dalam blok using melakukan unsubscribe di Dispose dan berhenti menerima event. Ini salah satu memory leak paling umum di kode desktop dan server .NET, terutama dengan event static dan service yang dipakai di seluruh aplikasi, yang hidup sampai proses berakhir.

Aturannya: siapa pun yang subscribe ke event milik object yang umurnya lebih panjang darinya harus unsubscribe, biasanya di Dispose (lihat pernyataan using). Saat publisher dan subscriber punya umur yang sama, seperti sebuah form dan tombol-tombolnya sendiri, unsubscribe tidak diperlukan.

Accessor add dan remove buatan sendiri

Sebuah event bisa menentukan apa yang dilakukan += dan -=, seperti get dan set pada property. Hal ini jarang dipakai, tetapi begitulah cara framework menyimpan handler di dictionary bersama atau meneruskannya ke object lain:

private EventHandler closed;

public event EventHandler Closed
{
    add    { Console.WriteLine("subscriber added");   closed += value; }
    remove { Console.WriteLine("subscriber removed"); closed -= value; }
}

Dengan accessor buatan sendiri, class memunculkan event lewat backing field-nya (closed?.Invoke(this, EventArgs.Empty)), karena event itu sendiri tidak lagi punya penyimpanan.

Pertanyaan yang sering diajukan

Apa itu event di C#?

Event adalah member class yang memungkinkan object lain meminta diberi tahu saat sesuatu terjadi. Event didukung oleh multicast delegate, tetapi kode dari luar hanya bisa menambahkan handler (+=) dan menghapusnya (-=). Hanya class yang mendeklarasikan event yang bisa memunculkannya.

Apa bedanya event dan delegate di C#?

Delegate adalah tipe yang mereferensikan method. Event adalah member yang dideklarasikan dengan tipe delegate ditambah keyword event, yang membatasi aksesnya: dari luar class kamu tidak bisa memanggilnya, membaca daftar handler-nya, atau meng-assign-nya dengan =, hanya subscribe dan unsubscribe. Field delegate public mengizinkan semua itu, jadi subscriber mana pun bisa menghapus subscriber lain atau memicu event-nya.

Bagaimana cara mengirim data lewat event C#?

Turunkan sebuah class dari EventArgs dengan datanya sebagai property read-only (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) dan deklarasikan event-nya sebagai EventHandler<OrderPlacedEventArgs>. Munculkan event itu dengan OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.

Bagaimana cara memunculkan event dengan aman di C#?

Pakai MyEvent?.Invoke(this, args);. Event tanpa subscriber bernilai null, jadi memanggilnya secara langsung melempar NullReferenceException. Operator ?. membaca field-nya sekali, yang juga menghindari race condition saat thread lain menghapus handler terakhir di antara pengecekan null dan pemanggilannya.

Apakah event C# bisa menyebabkan memory leak?

Bisa. Subscribe menyimpan delegate yang mereferensikan subscriber, jadi publisher membuat subscriber tetap hidup selama publisher itu hidup. Publisher yang berumur panjang (event static, service yang dipakai di seluruh aplikasi) dengan subscriber berumur pendek yang tidak pernah unsubscribe akan menahan setiap subscriber itu di memori. Lakukan unsubscribe dengan -= saat subscriber selesai dipakai, biasanya di Dispose.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai