Foto oleh Ann H via Pexels

Delegate adalah tipe yang merepresentasikan referensi ke sebuah method, memungkinkan method diperlakukan seperti data yang bisa disimpan dalam variabel, dikirim sebagai parameter, atau dipanggil secara dinamis tanpa mengetahui implementasinya secara langsung saat kode ditulis.

Action dan Func adalah delegate generic bawaan C# yang menghilangkan kebutuhan mendefinisikan delegate custom untuk kasus umum: Action untuk method yang tidak mengembalikan nilai (void), dan Func untuk method yang mengembalikan nilai tertentu.

Event dibangun di atas delegate, menyediakan mekanisme publish-subscribe yang memungkinkan satu class memberi notifikasi ke banyak subscriber lain ketika sesuatu terjadi, pola yang sangat umum dipakai dalam pemrograman berbasis UI maupun arsitektur event-driven.

Mendefinisikan dan Memakai Delegate

Delegate menyimpan referensi method yang bisa diganti-ganti

public delegate int Operasi(int a, int b); public static int Tambah(int a, int b) => a + b; public static int Kali(int a, int b) => a * b; Operasi op = Tambah; Console.WriteLine(op(3, 4)); -- Expected: 7 op = Kali; Console.WriteLine(op(3, 4)); -- Expected: 12
Variabel bertipe delegate Operasi bisa menunjuk ke method mana pun yang memiliki signature sesuai, baik Tambah maupun Kali, dan bisa diubah untuk menunjuk method berbeda kapan pun diperlukan, memberikan fleksibilitas untuk memilih logika yang dijalankan secara dinamis saat runtime.

Action dan Func sebagai Delegate Bawaan

Action untuk void, Func untuk method dengan return value

Action<string> cetakPesan = pesan => Console.WriteLine(pesan); cetakPesan("Halo Dunia"); -- Expected: Halo Dunia Func<int, int, int> tambahFunc = (a, b) => a + b; Console.WriteLine(tambahFunc(5, 3)); -- Expected: 8
Action merepresentasikan method yang menerima satu parameter string dan tidak mengembalikan nilai, sementara Func merepresentasikan method yang menerima dua parameter int dan mengembalikan int (parameter terakhir pada Func selalu merupakan tipe return value-nya), menghilangkan kebutuhan mendefinisikan delegate custom untuk kasus-kasus umum seperti ini.

Event untuk Pola Publish-Subscribe

Event memberi notifikasi ke subscriber saat dipicu

public class Tombol { public event Action OnDiklik; public void Klik() => OnDiklik?.Invoke(); } Tombol tombolSaya = new Tombol(); tombolSaya.OnDiklik += () => Console.WriteLine("Tombol diklik!"); tombolSaya.Klik(); -- Expected: Tombol diklik!
Event dideklarasikan dengan kata kunci event di atas tipe delegate, dan subscriber bisa mendaftarkan method mereka dengan operator += tanpa bisa langsung memanggil event tersebut dari luar class (berbeda dengan delegate biasa), menjaga agar hanya class pemilik event yang bisa memicunya secara langsung.

Multicast Delegate untuk Banyak Subscriber

Satu event bisa dipanggil oleh banyak subscriber sekaligus

Tombol tombolSaya = new Tombol(); tombolSaya.OnDiklik += () => Console.WriteLine("Log: tombol diklik"); tombolSaya.OnDiklik += () => Console.WriteLine("Analitik: mencatat klik"); tombolSaya.Klik(); -- Expected: kedua pesan tercetak, KEDUA subscriber dipanggil
Delegate dan event di C# bersifat multicast, artinya satu event bisa memiliki banyak subscriber sekaligus yang semuanya akan dipanggil berurutan ketika event tersebut dipicu, memungkinkan satu aksi seperti klik tombol memicu beberapa reaksi berbeda (logging, analitik, update UI) tanpa saling mengetahui satu sama lain.

Memahami Multicast Delegate pada Penanganan Event

Satu event dapat dilanggani oleh lebih dari satu delegate (multicast)

public delegate void LogHandler(string pesan); class Logger { public event LogHandler? OnLog; public void Proses() { OnLog?.Invoke("Proses dimulai"); } } var logger = new Logger(); logger.OnLog += pesan => Console.WriteLine("Console: " + pesan); logger.OnLog += pesan => Console.WriteLine("File: " + pesan); logger.Proses(); // Output: // Console: Proses dimulai // File: Proses dimulai

Delegate pada C# bersifat multicast, artinya satu instance delegate dapat menyimpan referensi ke lebih dari satu method sekaligus menggunakan operator +=, dan ketika delegate tersebut dipanggil (Invoke), seluruh method yang terdaftar akan dijalankan secara berurutan sesuai urutan pendaftarannya. Pada contoh di atas, dua handler berbeda yang didaftarkan ke event OnLog sama-sama dijalankan ketika method Proses() memanggil OnLog?.Invoke.

Tanda tanya pada OnLog?.Invoke adalah null-conditional operator yang mencegah NullReferenceException ketika belum ada satu pun subscriber yang mendaftar ke event tersebut, karena nilai default sebuah event sebelum ada yang subscribe adalah null. Tanpa pengecekan ini, memanggil Invoke langsung pada event yang masih null akan menyebabkan aplikasi crash saat runtime.

Action dan Func merupakan delegate generic bawaan .NET yang sering dipakai sebagai pengganti pendeklarasian delegate kustom; Action dipakai untuk method yang tidak mengembalikan nilai (void), sedangkan Func dipakai untuk method yang mengembalikan nilai, dengan parameter generic terakhir pada Func selalu merepresentasikan tipe data yang dikembalikan.

Func, Predicate, dan Lambda Expression untuk Komposisi Logika

Func, Predicate, dan Action adalah delegate generic bawaan .NET

Func<int, int, int> tambah = (a, b) => a + b; Predicate<int> isGenap = angka => angka % 2 == 0; Action<string> cetak = pesan => Console.WriteLine(pesan); Console.WriteLine(tambah(5, 3)); // Output: 8 Console.WriteLine(isGenap(10)); // Output: True List<int> angkaList = new List<int> { 1, 2, 3, 4, 5, 6 }; List<int> genap = angkaList.FindAll(isGenap); cetak(string.Join(",", genap)); // Output: 2,4,6

Func, Predicate, dan Action adalah delegate generic bawaan .NET yang menghilangkan kebutuhan mendeklarasikan tipe delegate kustom untuk setiap kebutuhan berbeda. Func dipakai untuk method yang mengembalikan nilai dengan parameter generic terakhir selalu merepresentasikan tipe hasil, seperti Func pada contoh tambah yang menerima dua int dan mengembalikan int, sedangkan Action dipakai untuk method tanpa nilai kembali seperti cetak.

Predicate secara khusus merupakan delegate yang selalu mengembalikan bool dan menerima satu parameter, umum dipakai sebagai kriteria pencocokan pada method seperti FindAll, Find, atau Exists milik List, sebagaimana terlihat pada isGenap yang dipakai langsung sebagai argumen FindAll untuk menyaring angka genap dari daftar. Secara fungsional, Predicate setara dengan Func, namun penamaannya lebih mengekspresikan maksud sebagai fungsi pemeriksaan kondisi.

Lambda expression, ditulis dengan sintaks parameter diikuti tanda panah (=>) dan isi ekspresi atau blok kode, menjadi cara paling ringkas untuk membuat instance dari delegate semacam ini tanpa perlu mendefinisikan method bernama secara terpisah. Komposisi logika menggunakan Func dan Predicate memungkinkan perilaku suatu method diteruskan sebagai argumen ke method lain, sebuah pola yang menjadi dasar dari banyak operator LINQ seperti Where dan Select. Pendekatan komposisi semacam ini membuat logika bisnis yang bersifat kondisional dapat diteruskan sebagai parameter ke method generik, sehingga satu method pemroses data dapat dipakai ulang untuk berbagai kriteria berbeda tanpa perlu menulis ulang method serupa untuk setiap kriteria baru.

Unsubscribe Event Handler untuk Mencegah Memory Leak

public class Tombol { public event Action? Diklik; public void Klik() => Diklik?.Invoke(); } var tombol = new Tombol(); Action handler = () => Console.WriteLine("Tombol diklik!"); tombol.Diklik += handler; tombol.Klik(); tombol.Diklik -= handler; tombol.Klik(); // Output: // Tombol diklik!

Setiap kali sebuah object berlangganan event milik object lain menggunakan operator +=, publisher event secara internal menyimpan referensi ke subscriber tersebut di dalam daftar delegate miliknya. Selama langganan itu tidak dilepas dengan operator -=, garbage collector tidak akan pernah membersihkan subscriber dari memori walaupun objek tersebut sudah tidak digunakan lagi di bagian lain program, karena masih ada referensi aktif yang menahannya tetap hidup selama publisher event masih ada.

Pada contoh di atas, setelah handler dilepas dengan tombol.Diklik -= handler, pemanggilan Klik() berikutnya tidak lagi mencetak apa pun karena tidak ada subscriber yang tersisa pada daftar event tersebut. Kelalaian melepas event handler, khususnya pada aplikasi desktop atau UI dengan banyak komponen yang dibuat dan dihancurkan berulang kali selama siklus hidup aplikasi, adalah penyebab umum memory leak yang sulit dilacak pada aplikasi .NET berskala besar.

Kesalahan Umum yang Sering Terjadi

Lupa Memeriksa Null Sebelum Memanggil Event

Memanggil event secara langsung tanpa operator null-conditional (?.Invoke()) akan menghasilkan NullReferenceException jika belum ada subscriber yang mendaftar sama sekali ke event tersebut.

Lupa Melepas Subscription Event yang Sudah Tidak Dibutuhkan

Subscriber yang tidak melepas langganannya dengan operator -= ketika sudah tidak dibutuhkan bisa menyebabkan memory leak, karena object yang menjadi subscriber tidak bisa dibersihkan garbage collector selama masih terdaftar di event tersebut.

Menggunakan Delegate Custom Padahal Action/Func Sudah Cukup

Mendefinisikan delegate custom untuk kasus sederhana yang sebenarnya sudah bisa ditangani Action atau Func menambah kode yang tidak perlu; delegate custom sebaiknya dibuat hanya ketika nama yang deskriptif benar-benar memberikan nilai tambah pada keterbacaan kode.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Action dan Func memberikan kenyamanan tanpa perlu mendefinisikan delegate baru untuk kasus umum, namun delegate custom dengan nama yang bermakna (seperti Operasi pada contoh di atas) bisa membuat kode lebih mudah dipahami untuk domain bisnis yang spesifik.

Event memberikan enkapsulasi yang lebih baik dibanding delegate publik biasa karena membatasi siapa yang bisa memicu notifikasi tersebut, namun menambah sedikit kompleksitas dibanding pemanggilan method langsung untuk kasus yang sebenarnya tidak membutuhkan pola publish-subscribe.

Ringkasan

Delegate, Action, Func, dan event membentuk fondasi pemrograman fungsional dan event-driven di C#, memungkinkan method diperlakukan sebagai data dan notifikasi dikirim ke banyak subscriber sekaligus. Pilih Action/Func untuk kebutuhan sederhana, dan event ketika benar-benar membutuhkan pola publish-subscribe dengan enkapsulasi yang baik.

Seri Belajar C# Fundamental:

← Sebelumnya: LINQ Dasar untuk Query Collection

→ Selanjutnya: Async Await dan Asynchronous Programming di C#