Memahami C# Delegate: Konsep, Cara Kerja, dan Contoh Penggunaan
5 Agustus 2026
•
1 jam baca
•
15 pembaca

Delegate adalah tipe data referensi di C# yang menyimpan referensi ke method, sehingga method bisa diperlakukan sebagai data. Pembahasan mulai dari konsep dasar, multicast, event, closure, hingga praktik terbaik dan analisis performa.
Bagian 1: Pendahuluan
Apa itu Delegate?
Delegate adalah tipe data referensi di C# yang menyimpan referensi ke sebuah method, alih-alih menyimpan nilai atau objek biasa. Karena Delegate menyimpan referensi ke method, ia memungkinkan sebuah method diperlakukan sebagai data yang bisa disimpan dalam variabel, dikirim sebagai parameter, dikembalikan dari method lain, atau dipanggil secara dinamis pada saat runtime.
Secara sederhana, jika biasanya sebuah variabel menyimpan angka, string, atau objek, maka Delegate menyimpan "alamat" dari sebuah method beserta signature-nya (tipe parameter dan tipe return value). Selama sebuah method memiliki signature yang cocok dengan Delegate tersebut, method itu bisa "dititipkan" ke dalam instance Delegate dan dipanggil kapan saja melalui instance tersebut.
Contoh sederhana:
C#
public delegate void Notify(string message);public class Program{public static void SendEmail(string message){Console.WriteLine($"Email sent: {message}");}public static void Main(){Notify notify = SendEmail;notify("Order has been placed.");}}
Pada contoh di atas, Notify adalah tipe Delegate yang mendefinisikan signature: menerima satu parameter string dan tidak mengembalikan nilai (void). Method SendEmail memiliki signature yang cocok, sehingga bisa ditugaskan ke variabel notify dan dipanggil layaknya memanggil method biasa.
Mengapa Delegate Dibuat?
Delegate diciptakan untuk menjawab kebutuhan agar method bisa diperlakukan sebagai first-class citizen, sama seperti variabel pada umumnya. Tanpa Delegate, sebuah class hanya bisa memanggil method yang sudah ia ketahui secara langsung pada saat compile time, sehingga perilaku suatu komponen menjadi kaku dan sulit diperluas tanpa mengubah kode sumbernya.
Beberapa alasan mendasar mengapa Delegate dibutuhkan dalam desain bahasa C#:
- Memungkinkan callback, yaitu memberi tahu kode pemanggil (caller) untuk menjalankan sebuah method tertentu setelah suatu proses selesai, tanpa caller tersebut perlu tahu detail implementasinya.
- Menjadi dasar bagi mekanisme event, yang banyak dipakai dalam pemrograman GUI dan arsitektur publisher-subscriber.
- Mendukung Higher Order Function, yaitu method yang menerima method lain sebagai parameter atau mengembalikan method sebagai hasilnya.
- Memberi fleksibilitas untuk menukar perilaku (behavior) suatu komponen pada saat runtime, tanpa mengubah struktur class yang sudah ada, sehingga selaras dengan prinsip Open/Closed dalam SOLID.
Sejarah Delegate di C#
Delegate sudah hadir sejak C# versi pertama (.NET Framework 1.0) dan menjadi salah satu fitur inti yang membedakan C# dari bahasa seperti Java pada masa itu, yang belum memiliki konsep function pointer yang aman secara tipe (type-safe). Desain awal Delegate terinspirasi dari konsep function pointer pada C dan C++, tetapi dibuat lebih aman karena Delegate di C# bersifat strongly typed, sehingga compiler dapat memverifikasi kecocokan signature sebelum program dijalankan.
Pada masa awal, penggunaan Delegate cenderung verbose karena harus dideklarasikan secara eksplisit menggunakan kata kunci delegate dan diberi nama method secara langsung. Seiring waktu, C# terus menambahkan sintaks yang menyederhanakan cara membuat dan menggunakan Delegate.
Evolusi Delegate hingga C# Terbaru
Beberapa titik penting evolusi Delegate sepanjang perkembangan C#:
- C# 1.0: Delegate diperkenalkan dengan sintaks eksplisit menggunakan kata kunci delegate, hanya bisa diisi dengan named method.
- C# 2.0: Diperkenalkan Anonymous Method menggunakan kata kunci delegate { ... }, sehingga method tidak perlu dideklarasikan terpisah. Generic Delegate juga mulai didukung penuh berkat generic type di C# 2.0.
- C# 3.0: Lambda Expression diperkenalkan dengan operator =>, menggantikan sebagian besar penggunaan Anonymous Method karena lebih ringkas. Pada versi ini pula tipe Delegate bawaan seperti Func dan Action mulai diperluas penggunaannya, didukung oleh LINQ yang bergantung besar pada Delegate.
- C# 5.0 ke atas: Delegate mulai berdampingan dengan async/await, memunculkan pola seperti Func<Task> dan Action versi asynchronous.
- C# terbaru (mendekati C# 12-13): Delegate tetap menjadi fondasi penting, terutama dengan semakin banyaknya static lambda dan optimisasi allocation, di mana compiler dan runtime terus disempurnakan agar penggunaan Delegate lebih efisien dari sisi memori dan performa.
Delegate Sebelum Lambda Expression
Sebelum Lambda Expression hadir di C# 3.0, penggunaan Delegate memerlukan salah satu dari dua pendekatan berikut:
-
Pendekatan pertama menggunakan named method:
C#
public delegate int Calculate(int a, int b);public class MathOperations{public static int Add(int a, int b) => a + b;}public class Program{public static void Main(){Calculate calculate = new Calculate(MathOperations.Add);int result = calculate(5, 3);Console.WriteLine(result);}} -
Pendekatan kedua menggunakan Anonymous Method yang diperkenalkan pada C# 2.0:
C#
Calculate calculate = delegate (int a, int b){return a + b;};int result = calculate(5, 3);
Kedua pendekatan ini terasa cukup verbose, terutama saat digunakan berulang kali untuk logika-logika sederhana.
Delegate Setelah Lambda Expression
Sejak C# 3.0, Lambda Expression menjadi cara yang jauh lebih ringkas untuk membuat instance Delegate tanpa perlu mendefinisikan method terpisah maupun menulis kata kunci delegate secara eksplisit.
C#
Calculate calculate = (a, b) => a + b;int result = calculate(5, 3);
Dengan hadirnya Lambda Expression, penggunaan Delegate menjadi jauh lebih natural, terutama ketika dipakai bersama tipe Delegate generic bawaan seperti Func<T, TResult>, Action<T>, dan Predicate<T>, yang akan dibahas lebih dalam pada bagian-bagian selanjutnya. Lambda Expression juga menjadi fondasi penting bagi LINQ, karena hampir seluruh operator LINQ seperti Where, Select, dan OrderBy menerima Delegate sebagai parameter, dan Lambda Expression menjadi cara paling umum untuk mengisinya.
Bagian 2: Konsep Dasar Delegate
Method sebagai First-Class Object
Dalam banyak bahasa pemrograman fungsional, method atau fungsi bisa diperlakukan sebagai objek biasa: disimpan dalam variabel, dikirim sebagai argumen, dan dikembalikan sebagai hasil dari fungsi lain. Konsep ini disebut first-class function atau first-class citizen.
C# adalah bahasa yang berbasis object-oriented, sehingga method secara alami terikat pada class atau struct tempat ia didefinisikan dan tidak bisa berdiri sendiri sebagai nilai. Delegate hadir sebagai jembatan yang membuat method di C# bisa diperlakukan layaknya first-class object, tanpa mengubah fondasi object-oriented dari bahasa itu sendiri. Delegate pada dasarnya adalah sebuah class khusus yang dibuat oleh compiler, yang tugasnya membungkus referensi ke method (dan, jika diperlukan, referensi ke instance objeknya) sehingga bisa diperlakukan seperti objek pada umumnya.
Dengan Delegate, sebuah method bisa:
- Disimpan dalam variabel, seperti Calculate calc = Add;
- Dikirim sebagai parameter ke method lain, seperti ProcessData(data, Validate);
- Dikembalikan sebagai hasil dari sebuah method, seperti GetValidator()
- Disimpan dalam struktur data seperti array atau List<T>
Function Pointer pada Bahasa Lain
Konsep menyimpan "alamat" sebuah fungsi bukan hal baru; bahasa seperti C dan C++ sudah lama memiliki function pointer. Namun function pointer pada C/C++ bekerja pada level yang sangat rendah: ia hanya menyimpan alamat memori dari fungsi, tanpa jaminan bahwa signature-nya benar-benar cocok pada saat runtime. Kesalahan pada function pointer sering kali baru terlihat saat program crash, karena compiler tidak selalu bisa memverifikasi kecocokan tipe secara ketat.
Contoh function pointer pada C, sebagai pembanding konsep:
C
int add(int a, int b) {return a + b;}int (*operation)(int, int) = add;int result = operation(5, 3);
Delegate di C# mengambil ide dasar yang sama, yaitu menyimpan referensi ke fungsi, tetapi dibungkus dengan keamanan tipe (type safety) penuh dari CLR (Common Language Runtime). Setiap Delegate memiliki signature yang jelas, sehingga compiler akan menolak penugasan method yang signature-nya tidak cocok, jauh sebelum program dijalankan.
Bahasa lain seperti Java secara historis tidak memiliki function pointer maupun Delegate; sebagai gantinya Java mengandalkan interface dengan satu method (yang belakangan disebut functional interface) untuk mencapai efek serupa, baru diperkenalkan secara lebih ringkas melalui Lambda Expression pada Java 8.
Bagaimana C# Mengimplementasikan Delegate
Secara internal, setiap kali kita mendeklarasikan sebuah tipe Delegate menggunakan kata kunci delegate, compiler C# sebenarnya menghasilkan sebuah class baru yang secara implisit mewarisi dari System.MulticastDelegate (yang pada gilirannya mewarisi dari System.Delegate). Class ini dibuat otomatis oleh compiler; kita sebagai developer tidak perlu (dan tidak bisa) menulis class tersebut secara manual.
Contoh deklarasi:
C#
public delegate int Calculate(int a, int b);
Baris di atas, jika dikompilasi, kurang lebih setara secara konsep dengan class berikut:
C#
public sealed class Calculate : System.MulticastDelegate{public Calculate(object target, IntPtr method) { /* implementasi internal CLR */ }public int Invoke(int a, int b) { /* implementasi internal CLR */ }public IAsyncResult BeginInvoke(int a, int b, AsyncCallback callback, object state) { /* ... */ }public int EndInvoke(IAsyncResult result) { /* ... */ }}
Class hasil generate ini otomatis memiliki beberapa method penting:
- Constructor, yang menerima target objek (jika method-nya adalah instance method) dan pointer ke method.
- Invoke, method yang dipanggil saat kita "memanggil" Delegate seperti memanggil fungsi biasa.
- BeginInvoke dan EndInvoke, yang mendukung pemanggilan method secara asynchronous menggunakan pola lama (akan dibahas lebih lanjut pada Bagian 22).
Cara Kerja Delegate
Ketika sebuah method ditugaskan ke Delegate, ada dua informasi penting yang disimpan oleh instance Delegate tersebut:
- Method pointer, yaitu referensi ke lokasi method yang akan dijalankan.
- Target object, yaitu referensi ke instance objek tempat method tersebut berada — hanya relevan jika method yang ditugaskan adalah instance method, bukan static method.
Ketika Delegate dipanggil (di-invoke), CLR akan menggunakan kedua informasi tersebut untuk menentukan method mana yang harus dijalankan, dan pada instance objek yang mana (jika diperlukan).
C#
public class OrderService{public void ProcessOrder(string orderId){Console.WriteLine($"Processing order {orderId}");}}public delegate void OrderHandler(string orderId);public class Program{public static void Main(){var service = new OrderService();OrderHandler handler = service.ProcessOrder;handler("ORD-001");}}
Pada contoh di atas, handler menyimpan referensi ke method ProcessOrder sekaligus referensi ke instance service. Saat handler("ORD-001") dipanggil, CLR akan memanggil service.ProcessOrder("ORD-001") di balik layar.
Memory Model Delegate
Dari sisi memori, instance Delegate adalah objek yang dialokasikan di heap, sama seperti objek class pada umumnya, karena Delegate memang merupakan reference type. Setiap instance Delegate menyimpan sebuah invocation list, yaitu daftar pasangan (target object, method pointer) yang akan dipanggil saat Delegate tersebut di-invoke.
Untuk Delegate yang hanya menyimpan satu method (singlecast), invocation list ini hanya berisi satu entri. Namun Delegate di C# secara default sebenarnya selalu bersifat multicast, sehingga invocation list-nya bisa berisi lebih dari satu entri apabila beberapa method digabungkan menggunakan operator += (dibahas lebih dalam pada Bagian 6).
Beberapa implikasi penting dari memory model ini:
- Setiap kali sebuah lambda expression atau anonymous method yang meng-capture variabel dari luar dibuat, C# akan mengalokasikan objek closure tambahan di heap untuk menyimpan variabel yang di-capture tersebut, sehingga bisa berdampak pada performa apabila dilakukan berulang kali dalam loop atau hot path (akan dibahas lebih detail pada Bagian 32 tentang Performance).
- Karena Delegate adalah reference type, dua instance Delegate yang menyimpan method dan target yang sama tetap dianggap setara (equal) secara nilai, berkat override Equals pada System.Delegate yang membandingkan target dan method pointer, bukan alamat objek Delegate itu sendiri.
- Delegate yang menyimpan referensi ke instance object dapat secara tidak sengaja mencegah objek tersebut dibersihkan oleh Garbage Collector selama Delegate itu masih direferensikan oleh pihak lain, sebuah isu yang sering muncul dalam penggunaan Event dan akan dibahas pada Bagian 31 mengenai Anti Pattern.
Bagian 3: Deklarasi Delegate
Syntax
Deklarasi Delegate di C# menggunakan kata kunci delegate, diikuti dengan return type, nama tipe Delegate, dan daftar parameter, mirip seperti mendeklarasikan signature sebuah method tanpa isi (body).
Bentuk umum:
C#
[modifier] delegate [return type] [nama delegate] ([daftar parameter]);
Contoh:
C#
public delegate void Logger(string message);
Deklarasi ini bisa diletakkan pada level namespace, di dalam class, atau di dalam struct, tergantung seberapa luas cakupan penggunaan Delegate tersebut dibutuhkan. Delegate yang dideklarasikan di dalam class hanya bisa diakses melalui nama class tersebut apabila memiliki modifier akses yang sesuai, sama seperti anggota class pada umumnya.
Signature
Signature dari sebuah Delegate mencakup dua hal: tipe dari setiap parameter (beserta urutannya) dan tipe dari return value. Sebuah method hanya bisa ditugaskan ke suatu Delegate apabila signature method tersebut identik dengan signature Delegate-nya.
C#
public delegate int Operation(int a, int b);public class Calculator{public static int Add(int a, int b) => a + b;public static double Add(double a, double b) => a + b;}Operation op1 = Calculator.Add; // cocok, karena versi (int, int) mengembalikan int
Perlu dicatat bahwa nama parameter tidak diperhitungkan dalam pencocokan signature, hanya tipe dan urutannya. Nama parameter pada Delegate hanya berfungsi sebagai dokumentasi.
Return Value
Delegate bisa memiliki return type apa pun, termasuk void jika tidak mengembalikan nilai. Sebuah method hanya bisa ditugaskan ke Delegate apabila return type-nya sama persis (bukan sekadar compatible melalui implicit conversion, kecuali untuk kasus covariance yang akan dibahas pada Bagian 19).
C#
public delegate string Formatter(int value);public class Program{public static string FormatCurrency(int value) => $"Rp{value:N0}";public static void Main(){Formatter formatter = FormatCurrency;Console.WriteLine(formatter(150000));}}
Parameter
Delegate mendukung seluruh jenis parameter yang didukung oleh method biasa di C#, termasuk parameter opsional, params, ref, out, dan in.
Contoh dengan parameter out:
C#
public delegate bool TryParseHandler(string input, out int result);public class Program{public static bool TryParseNumber(string input, out int result){return int.TryParse(input, out result);}public static void Main(){TryParseHandler parser = TryParseNumber;if (parser("123", out int number)){Console.WriteLine($"Parsed: {number}");}}}
Contoh dengan params:
C#
public delegate int SumHandler(params int[] numbers);public class Program{public static int Sum(params int[] numbers){int total = 0;foreach (var n in numbers) total += n;return total;}public static void Main(){SumHandler sum = Sum;Console.WriteLine(sum(1, 2, 3, 4));}}
Delegate Type
Setiap deklarasi Delegate sebenarnya membentuk sebuah tipe baru, bukan sekadar alias dari method. Ini berarti dua Delegate yang memiliki signature identik namun nama tipe berbeda tetap dianggap sebagai dua tipe yang berbeda, sehingga tidak bisa saling ditukar secara langsung tanpa konversi eksplisit.
C#
public delegate int Calculate(int a, int b);public delegate int MathOperation(int a, int b);Calculate calc = (a, b) => a + b;// Baris di bawah ini tidak valid meskipun signature-nya sama:// MathOperation math = calc;// Harus dikonversi secara eksplisit:MathOperation math = new MathOperation(calc);
Bagian 4: Cara Kerja Delegate
Delegate Menyimpan Referensi Method
Inti dari sebuah Delegate adalah kemampuannya menyimpan referensi ke method, bukan menyalin isi (body) method tersebut. Ini berarti ketika sebuah method ditugaskan ke Delegate, yang disimpan hanyalah "alamat" atau pointer menuju method itu di memori, beserta informasi objek target apabila method tersebut adalah instance method.
Karena yang disimpan adalah referensi, perubahan pada objek target akan tetap tercermin saat Delegate dipanggil, karena Delegate tidak pernah menyimpan salinan state, melainkan hanya menyimpan ke mana ia harus mengarah saat di-invoke.
C#
public class Counter{public int Value { get; private set; }public void Increment() => Value++;}public delegate void Action0();public class Program{public static void Main(){var counter = new Counter();Action0 increment = counter.Increment;increment();increment();increment();Console.WriteLine(counter.Value); // 3}}
Pada contoh di atas, increment tidak menyimpan nilai Value itu sendiri, melainkan hanya menyimpan referensi ke method Increment pada instance counter. Setiap kali increment() dipanggil, ia benar-benar memanggil counter.Increment(), sehingga perubahan pada counter.Value selalu ter-update dengan benar.
Strongly Typed
Delegate di C# bersifat strongly typed, artinya compiler akan memvalidasi kecocokan tipe antara Delegate dan method yang ditugaskan kepadanya pada saat compile time, bukan pada saat runtime. Sifat ini yang membedakan Delegate dari function pointer mentah pada bahasa seperti C, yang rawan menimbulkan bug fatal apabila signature-nya tidak benar-benar cocok saat program dijalankan.
C#
public delegate int Validator(string input);public class Program{public static bool IsValid(string input) => !string.IsNullOrEmpty(input);public static void Main(){// Baris di bawah tidak akan pernah lolos compile,// karena return type IsValid (bool) tidak cocok dengan Validator (int).// Validator validator = IsValid;}}
Karena validasi ini terjadi di compile time, kesalahan semacam ini akan langsung terdeteksi sebelum kode sempat dijalankan, jauh berbeda dengan pendekatan function pointer konvensional yang baru bisa diketahui gagal setelah program berjalan dan menemui error runtime.
Signature Matching
Proses signature matching adalah proses yang dilakukan compiler untuk memastikan bahwa jumlah parameter, tipe parameter (beserta modifier seperti ref, out, atau in), urutan parameter, dan tipe return value dari method yang ditugaskan benar-benar identik dengan signature Delegate.
Signature matching juga berlaku untuk method generic, di mana compiler akan mencocokkan tipe hasil substitusi generic terhadap signature Delegate, bukan definisi generic method itu sendiri.
C#
public delegate bool Comparer(int a, int b);public class Program{public static bool IsGreater(int a, int b) => a > b;public static bool IsGreaterOrEqual(int x, int y) => x >= y; // nama parameter berbeda, tetap cocokpublic static void Main(){Comparer comparer1 = IsGreater; // cocokComparer comparer2 = IsGreaterOrEqual; // tetap cocok, karena nama parameter tidak diperhitungkan}}
Runtime Invocation
Meskipun validasi tipe dilakukan pada compile time, pemanggilan aktual method yang direferensikan oleh Delegate baru benar-benar terjadi pada runtime, saat Invoke (atau pemanggilan langsung seperti delegateInstance()) dijalankan.
C#
public delegate void Step();public class Program{public static void Main(){Step step = () => Console.WriteLine("Step 1");step += () => Console.WriteLine("Step 2");step += () => Console.WriteLine("Step 3");step(); // runtime invocation akan menjalankan ketiga method secara berurutan}}
Proses runtime invocation secara umum melalui langkah berikut:
- CLR membaca invocation list dari instance Delegate.
- Untuk setiap entri dalam invocation list, CLR mengambil pasangan target object dan method pointer.
- CLR memanggil method tersebut pada target object yang bersangkutan (atau langsung, jika method-nya static).
- Jika Delegate memiliki return value dan berisi lebih dari satu method (multicast), hanya nilai kembalian dari method terakhir dalam invocation list yang akan dikembalikan sebagai hasil pemanggilan Delegate secara keseluruhan (dibahas lebih lanjut pada Bagian 6 mengenai Multicast Delegate).
Visualisasi Internal Object Delegate
Secara konsep, sebuah instance Delegate dapat dibayangkan sebagai objek yang menyimpan struktur berikut:
Plain Text
Delegate Instance├── Target Object -> referensi ke instance objek (null jika static method)├── Method Pointer -> alamat method yang akan dijalankan└── Invocation List -> daftar (Target Object, Method Pointer) untuk kasus multicast├── Entry 1: (Target A, Method A)├── Entry 2: (Target B, Method B)└── Entry 3: (Target C, Method C)
Saat Delegate hanya menampung satu method (singlecast), invocation list secara internal tetap ada namun hanya berisi satu entri. Saat method digabungkan menggunakan +=, CLR akan membentuk instance Delegate baru dengan invocation list yang sudah diperluas, karena Delegate bersifat immutable: operasi += dan -= sebenarnya tidak mengubah instance Delegate yang sudah ada, melainkan membuat instance baru yang berisi invocation list hasil gabungan atau pengurangan.
C#
Step step1 = () => Console.WriteLine("A");Step step2 = step1;step1 += () => Console.WriteLine("B");// step2 tidak berubah, karena step1 += ... membuat instance baru,// bukan memodifikasi instance yang sudah ada.
Pemahaman bahwa Delegate bersifat immutable ini penting terutama saat membahas Multicast Delegate pada Bagian 6, karena kesalahpahaman terhadap sifat ini sering menjadi sumber bug, misalnya saat seorang developer mengira menambahkan method ke satu variabel Delegate akan otomatis memengaruhi variabel lain yang sebelumnya menyimpan referensi yang sama.
Bagian 5: Singlecast Delegate
Definisi
Singlecast Delegate adalah istilah yang digunakan untuk menggambarkan sebuah instance Delegate yang hanya menyimpan satu method dalam invocation list-nya. Perlu ditekankan bahwa di C# sebenarnya tidak ada tipe khusus bernama "Singlecast Delegate" secara terpisah dari "Multicast Delegate": semua Delegate di C# secara internal adalah turunan dari System.MulticastDelegate, hanya saja ketika invocation list-nya berisi satu entri saja, secara konseptual ia disebut sebagai Singlecast Delegate.
Istilah ini berguna untuk membedakan skenario penggunaan: kapan sebuah Delegate dimaksudkan untuk menampung tepat satu method (misalnya untuk merepresentasikan satu strategi atau satu handler), dan kapan ia dimaksudkan untuk menampung banyak method sekaligus (dibahas pada Bagian 6).
Cara Membuat
Membuat Singlecast Delegate dilakukan dengan cara yang sama seperti deklarasi Delegate pada umumnya, hanya saja hanya satu method saja yang ditugaskan kepadanya, tanpa menggunakan operator +=.
C#
public delegate decimal CalculatePrice(decimal basePrice);public class Program{public static decimal ApplyTax(decimal basePrice) => basePrice * 1.11m;public static void Main(){CalculatePrice calculatePrice = ApplyTax;Console.WriteLine(calculatePrice(100000));}}
Assign Method
Penugasan method ke Delegate bisa dilakukan dengan beberapa cara: melalui named method (static maupun instance), anonymous method, atau lambda expression, selama signature-nya cocok dengan tipe Delegate yang dituju.
C#
public delegate bool Validator(string input);public class Program{public static void Main(){// Menggunakan named methodValidator validator1 = IsNotEmpty;// Menggunakan lambda expressionValidator validator2 = input => !string.IsNullOrWhiteSpace(input);// Menggunakan anonymous methodValidator validator3 = delegate (string input){return input.Length > 3;};}public static bool IsNotEmpty(string input) => !string.IsNullOrEmpty(input);}
Penugasan ulang ke variabel Delegate yang sama akan menggantikan method sebelumnya sepenuhnya, bukan menambahkannya, karena penugasan menggunakan = berbeda dengan penggabungan menggunakan +=.
C#
Validator validator = input => input.Length > 0;validator = input => input.Length > 5; // method sebelumnya sepenuhnya digantikan
Invoke
Sebuah Singlecast Delegate dapat dipanggil dengan dua cara: memanggilnya langsung layaknya sebuah method, atau memanggil method Invoke secara eksplisit. Kedua cara ini secara fungsional identik; pemanggilan langsung sebenarnya hanya syntactic sugar yang oleh compiler diterjemahkan menjadi pemanggilan Invoke.
C#
Validator validator = input => input.Length >= 8;bool result1 = validator("password123"); // pemanggilan langsungbool result2 = validator.Invoke("password123"); // pemanggilan eksplisit melalui Invoke
C# versi terbaru juga mendukung null-conditional invocation menggunakan ?.Invoke(), yang berguna untuk menghindari NullReferenceException apabila Delegate yang dipanggil kemungkinan bernilai null, situasi yang umum terjadi terutama saat bekerja dengan Event.
C#
Validator? validator = null;bool? result = validator?.Invoke("test"); // aman, hasilnya null karena validator adalah null
Bagian 6: Multicast Delegate
Menggabungkan Delegate
Multicast Delegate adalah kondisi di mana sebuah instance Delegate menyimpan lebih dari satu method dalam invocation list-nya, sehingga satu kali pemanggilan Delegate akan menjalankan seluruh method yang tergabung di dalamnya secara berurutan. Kemampuan ini menjadikan Delegate sangat berguna untuk skenario di mana beberapa aksi perlu dijalankan sebagai respons terhadap satu kejadian yang sama, tanpa perlu memanggil masing-masing method secara manual satu per satu.
C#
public delegate void NotificationHandler(string message);public class Program{public static void SendEmail(string message) => Console.WriteLine($"[Email] {message}");public static void SendSms(string message) => Console.WriteLine($"[SMS] {message}");public static void Main(){NotificationHandler notify = SendEmail;notify += SendSms;notify("Order confirmed."); // menjalankan SendEmail lalu SendSms}}
Penggabungan method ke dalam sebuah Delegate dilakukan melalui method static Delegate.Combine, namun dalam praktiknya operator += jauh lebih umum digunakan karena lebih ringkas.
Operator +=
Operator += menambahkan method baru ke invocation list Delegate yang sudah ada. Karena Delegate bersifat immutable (sudah dibahas pada Bagian 4), operator ini sebenarnya membuat instance Delegate baru yang berisi gabungan invocation list lama ditambah method yang baru, kemudian menugaskan instance baru tersebut ke variabel yang bersangkutan.
C#
NotificationHandler notify = SendEmail;notify += SendSms;notify += message => Console.WriteLine($"[Push] {message}");
Method yang sama juga bisa ditambahkan lebih dari sekali ke invocation list yang sama, dan ia akan dipanggil sebanyak jumlah ia ditambahkan.
C#
NotificationHandler notify = SendEmail;notify += SendEmail; // SendEmail sekarang ada dua kali dalam invocation listnotify("Test"); // SendEmail akan dipanggil dua kali
Operator -=
Operator -= menghapus method tertentu dari invocation list. Penghapusan dilakukan dengan mencari kecocokan persis antara target object dan method pointer; apabila ditemukan lebih dari satu kecocokan, hanya kemunculan terakhir dalam invocation list yang akan dihapus.
C#
NotificationHandler notify = SendEmail;notify += SendSms;notify += SendEmail;notify -= SendEmail; // hanya menghapus satu kemunculan SendEmail (yang terakhir)notify("Test"); // SendEmail (pertama) dan SendSms tetap terpanggil
Perlu diperhatikan bahwa lambda expression atau anonymous method yang dibuat secara terpisah, meskipun terlihat identik secara logika, dianggap sebagai method yang berbeda oleh CLR sehingga tidak bisa dihapus menggunakan -= kecuali referensi Delegate-nya benar-benar sama persis (disimpan dalam variabel yang sama sebelumnya).
C#
NotificationHandler notify = message => Console.WriteLine(message);notify -= (message => Console.WriteLine(message)); // TIDAK menghapus apa pun, karena instance lambda berbeda
Invocation List
Invocation list adalah struktur internal yang menyimpan seluruh method yang tergabung dalam sebuah Multicast Delegate. Kita bisa mengakses invocation list ini secara terprogram melalui method GetInvocationList(), yang berguna misalnya untuk memeriksa berapa banyak method yang tergabung, atau untuk memanggil setiap method secara manual satu per satu (misalnya untuk menangani exception per method, dibahas di bagian berikutnya).
C#
NotificationHandler notify = SendEmail;notify += SendSms;Delegate[] handlers = notify.GetInvocationList();Console.WriteLine($"Jumlah handler: {handlers.Length}"); // 2foreach (var handler in handlers){Console.WriteLine(handler.Method.Name);}
Urutan Eksekusi
Method dalam invocation list dijalankan secara berurutan (sekuensial), sesuai urutan method tersebut ditambahkan, bukan secara paralel. Ini berarti method kedua baru akan mulai dijalankan setelah method pertama selesai sepenuhnya.
C#
NotificationHandler notify = message => Console.WriteLine($"Step 1: {message}");notify += message => Console.WriteLine($"Step 2: {message}");notify += message => Console.WriteLine($"Step 3: {message}");notify("Processing"); // Step 1, lalu Step 2, lalu Step 3, selalu dalam urutan ini
Karena eksekusinya bersifat sinkron dan berurutan, apabila salah satu method di tengah invocation list membutuhkan waktu lama untuk selesai (misalnya melakukan operasi I/O yang blocking), seluruh method setelahnya akan ikut tertunda.
Exception Handling
Salah satu hal penting yang perlu dipahami pada Multicast Delegate adalah perilakunya ketika salah satu method dalam invocation list melempar exception: seluruh proses pemanggilan akan langsung terhenti pada method yang melempar exception tersebut, dan method-method berikutnya dalam invocation list tidak akan dipanggil sama sekali.
C#
NotificationHandler notify = message => Console.WriteLine("Handler 1: " + message);notify += message => throw new InvalidOperationException("Handler 2 gagal");notify += message => Console.WriteLine("Handler 3: " + message); // TIDAK akan pernah dipanggiltry{notify("Test");}catch (InvalidOperationException ex){Console.WriteLine($"Error: {ex.Message}");}
Untuk menghindari perilaku "berhenti di tengah jalan" ini, pola yang umum digunakan adalah memanggil setiap method dalam invocation list secara manual menggunakan GetInvocationList(), sehingga exception pada satu handler tidak menghentikan handler lainnya.
C#
NotificationHandler notify = message => Console.WriteLine("Handler 1: " + message);notify += message => throw new InvalidOperationException("Handler 2 gagal");notify += message => Console.WriteLine("Handler 3: " + message);foreach (NotificationHandler handler in notify.GetInvocationList()){try{handler("Test");}catch (Exception ex){Console.WriteLine($"Salah satu handler gagal: {ex.Message}");}}// Handler 1 dan Handler 3 tetap terpanggil, meskipun Handler 2 melempar exception
Bagian 7: Delegate sebagai Parameter
Higher Order Function
Higher Order Function adalah istilah untuk method yang menerima method lain sebagai parameter, mengembalikan method sebagai hasilnya, atau bahkan melakukan keduanya. Delegate adalah mekanisme yang memungkinkan konsep Higher Order Function ini diterapkan di C#, karena tanpa Delegate, parameter suatu method hanya bisa berupa data biasa, bukan "perilaku" yang bisa dieksekusi.
Dengan menerima Delegate sebagai parameter, sebuah method bisa membiarkan pemanggilnya (caller) menentukan sendiri bagaimana suatu bagian dari logika dijalankan, tanpa method tersebut perlu tahu detail implementasinya.
C#
public delegate int Operation(int a, int b);public class Program{public static int Calculate(int a, int b, Operation operation){return operation(a, b);}public static void Main(){int sum = Calculate(5, 3, (a, b) => a + b);int product = Calculate(5, 3, (a, b) => a * b);Console.WriteLine(sum); // 8Console.WriteLine(product); // 15}}
Pada contoh di atas, method Calculate tidak perlu tahu apakah operasi yang dijalankan adalah penjumlahan, perkalian, atau operasi lain; ia hanya bertugas menjalankan Delegate yang diberikan kepadanya. Inilah esensi dari Higher Order Function: memisahkan apa yang dilakukan (dijalankan oleh method) dari bagaimana melakukannya (ditentukan oleh Delegate yang dikirim).
Callback Pattern
Callback Pattern adalah pola di mana sebuah method menerima Delegate sebagai parameter dengan tujuan agar Delegate tersebut dipanggil pada titik tertentu selama proses berjalan, biasanya setelah suatu tahapan selesai atau ketika suatu kejadian terjadi. Pola ini sangat umum digunakan untuk memberi tahu pemanggil tentang progres atau hasil dari sebuah proses, tanpa method yang menjalankan proses tersebut perlu mengetahui apa yang akan dilakukan setelah pemberitahuan itu.
C#
public delegate void ProgressCallback(int percentage);public class FileDownloader{public void Download(string url, ProgressCallback onProgress){for (int progress = 0; progress <= 100; progress += 25){// simulasi proses downloadonProgress(progress);}}}public class Program{public static void Main(){var downloader = new FileDownloader();downloader.Download("https://example.com/file.zip", percentage =>{Console.WriteLine($"Progress: {percentage}%");});}}
Callback juga umum digunakan untuk membedakan penanganan hasil sukses dan gagal, dengan menerima dua Delegate sekaligus sebagai parameter.
C#
public delegate void OnSuccess(string data);public delegate void OnError(string errorMessage);public class ApiClient{public void FetchData(bool simulateError, OnSuccess onSuccess, OnError onError){if (simulateError){onError("Gagal mengambil data dari server.");}else{onSuccess("{ \"status\": \"ok\" }");}}}public class Program{public static void Main(){var client = new ApiClient();client.FetchData(simulateError: false,onSuccess: data => Console.WriteLine($"Sukses: {data}"),onError: error => Console.WriteLine($"Error: {error}"));}}
Dependency Injection Sederhana
Delegate juga bisa dimanfaatkan sebagai bentuk sederhana dari Dependency Injection, terutama untuk kasus di mana yang perlu "disuntikkan" hanyalah satu perilaku spesifik, bukan seluruh implementasi dari sebuah interface. Pendekatan ini sering disebut juga sebagai functional dependency injection, karena dependency yang disuntikkan berupa fungsi, bukan objek dengan banyak method.
C#
public class PriceCalculator{private readonly Func<decimal, decimal> _taxStrategy;public PriceCalculator(Func<decimal, decimal> taxStrategy){_taxStrategy = taxStrategy;}public decimal CalculateFinalPrice(decimal basePrice){return basePrice + _taxStrategy(basePrice);}}public class Program{public static void Main(){var domesticCalculator = new PriceCalculator(price => price * 0.11m); // PPN 11%var internationalCalculator = new PriceCalculator(price => price * 0.05m); // pajak internasional 5%Console.WriteLine(domesticCalculator.CalculateFinalPrice(100000));Console.WriteLine(internationalCalculator.CalculateFinalPrice(100000));}}
Dibandingkan dengan Dependency Injection konvensional menggunakan interface, pendekatan berbasis Delegate ini lebih ringkas untuk kasus sederhana, namun kurang cocok apabila dependency yang disuntikkan memiliki banyak method terkait yang perlu dikelola sebagai satu kesatuan; untuk kasus semacam itu, interface tetap menjadi pilihan yang lebih tepat (dibahas lebih lanjut pada Bagian 29 mengenai perbandingan Delegate vs Interface).
Contoh: Sorting
C#
public class Program{public static void Main(){var numbers = new List<int> { 5, 2, 8, 1, 9 };numbers.Sort((a, b) => a.CompareTo(b)); // ascendingConsole.WriteLine(string.Join(", ", numbers));numbers.Sort((a, b) => b.CompareTo(a)); // descendingConsole.WriteLine(string.Join(", ", numbers));}}
Contoh: Payment Strategy
C#
public delegate bool PaymentStrategy(decimal amount);public class PaymentProcessor{public void Process(decimal amount, PaymentStrategy strategy){bool success = strategy(amount);Console.WriteLine(success ? "Pembayaran berhasil." : "Pembayaran gagal.");}}public class Program{public static bool PayWithCreditCard(decimal amount){Console.WriteLine($"Memproses kartu kredit sebesar {amount}");return true;}public static bool PayWithEWallet(decimal amount){Console.WriteLine($"Memproses e-wallet sebesar {amount}");return amount <= 5000000;}public static void Main(){var processor = new PaymentProcessor();processor.Process(2500000, PayWithCreditCard);processor.Process(6000000, PayWithEWallet);}}
Contoh: Validation
C#
public delegate bool ValidationRule(string input);public class Program{public static bool Validate(string input, params ValidationRule[] rules){foreach (var rule in rules){if (!rule(input)){return false;}}return true;}public static void Main(){bool isValid = Validate("budi123",input => !string.IsNullOrWhiteSpace(input),input => input.Length >= 6,input => input.Any(char.IsDigit));Console.WriteLine(isValid); // true}}
Bagian 8: Anonymous Method
Anonymous Method
Anonymous Method adalah cara untuk mendefinisikan blok kode yang ditugaskan langsung ke sebuah Delegate tanpa perlu membuat method terpisah dengan nama tertentu. Fitur ini diperkenalkan pada C# 2.0 sebagai jawaban atas kebutuhan menulis logika sederhana secara langsung di tempat Delegate tersebut digunakan, tanpa harus mendeklarasikan method baru hanya untuk dipakai satu kali.
Sebelum Anonymous Method hadir, setiap kali sebuah Delegate membutuhkan implementasi kecil dan spesifik, developer harus membuat method terpisah, meskipun method tersebut hanya akan dipakai pada satu tempat saja. Anonymous Method menghilangkan kebutuhan ini dengan mengizinkan blok kode ditulis langsung sebagai bagian dari ekspresi penugasan Delegate.
Kata Kunci delegate
Anonymous Method dituliskan menggunakan kata kunci delegate, diikuti dengan daftar parameter (opsional) dan blok kode di dalam kurung kurawal, tanpa nama method.
C#
public delegate int Calculate(int a, int b);public class Program{public static void Main(){Calculate add = delegate (int a, int b){return a + b;};Console.WriteLine(add(5, 3)); // 8}}
Anonymous Method juga bisa dituliskan tanpa parameter sama sekali, bahkan tanpa tanda kurung, apabila Delegate yang dituju tidak memiliki parameter.
C#
public delegate void Notify();public class Program{public static void Main(){Notify notify = delegate{Console.WriteLine("Notifikasi terkirim.");};notify();}}
Salah satu karakteristik unik dari Anonymous Method adalah kemampuannya untuk mengabaikan daftar parameter sepenuhnya menggunakan kata kunci delegate tanpa tanda kurung, bahkan ketika Delegate targetnya memiliki parameter. Dalam kasus ini, parameter tersebut diabaikan begitu saja di dalam body method.
C#
public delegate void LogHandler(string message, int severity);public class Program{public static void Main(){LogHandler logger = delegate{Console.WriteLine("Log diterima, parameter diabaikan.");};logger("Error terjadi", 3);}}
Kelebihan
Beberapa kelebihan Anonymous Method dibandingkan dengan named method konvensional:
- Tidak perlu mendeklarasikan method terpisah untuk logika yang hanya dipakai satu kali, sehingga kode menjadi lebih ringkas dan lebih mudah dibaca dalam konteks penggunaannya.
- Bisa mengakses (meng-capture) variabel dari lingkup (scope) di luar Anonymous Method itu sendiri, sebuah kemampuan yang disebut closure, yang akan dibahas lebih dalam pada Bagian 9.
- Mengurangi jarak antara deklarasi logika dan tempat logika tersebut digunakan, sehingga lebih mudah dipahami dibandingkan harus mencari method terpisah di bagian lain dari kode.
C#
public class Program{public static void Main(){int threshold = 10;Func<int, bool> isAboveThreshold = delegate (int value){return value > threshold; // meng-capture variabel 'threshold' dari luar};Console.WriteLine(isAboveThreshold(15)); // true}}
Kekurangan
Meskipun bermanfaat, Anonymous Method juga memiliki beberapa kekurangan yang membuatnya kini jarang dipakai, terutama setelah kehadiran Lambda Expression:
- Sintaksnya masih tergolong verbose dibandingkan Lambda Expression, terutama untuk logika yang sangat sederhana.
- Tidak bisa digunakan sebagai expression tree, yang dibutuhkan misalnya oleh LINQ to SQL atau Entity Framework untuk menerjemahkan logika C# menjadi query database; hanya Lambda Expression berbentuk expression lambda yang bisa dikonversi menjadi expression tree.
- Anonymous Method yang kompleks dan panjang cenderung membuat kode sulit dibaca, terutama apabila ditulis langsung sebagai argumen dari sebuah method call, sehingga mengaburkan alur logika utama.
- Sulit untuk melakukan debugging pada Anonymous Method yang bertumpuk (nested) di dalam Anonymous Method lain, karena stack trace yang dihasilkan cenderung kurang deskriptif dibandingkan named method.
Perbandingan: Method, Anonymous Method, dan Lambda
Ketiga pendekatan berikut secara fungsional setara, namun berbeda dari sisi keringkasan sintaks dan kapan masing-masing lebih tepat digunakan.
Named Method, paling tepat digunakan apabila logika tersebut cukup kompleks, dipakai berulang kali di banyak tempat, atau perlu diuji secara terpisah (unit testing).
C#
public delegate bool IsEven(int number);public static bool CheckEven(int number) => number % 2 == 0;IsEven checkEven = CheckEven;
Anonymous Method, cocok untuk kasus transisi ketika perlu meng-capture variabel luar namun tetap membutuhkan blok kode dengan beberapa baris statement, meskipun saat ini jarang dipilih karena Lambda Expression mampu melakukan hal yang sama dengan sintaks lebih ringkas.
C#
IsEven checkEven = delegate (int number){return number % 2 == 0;};
Lambda Expression, menjadi pilihan paling umum saat ini karena paling ringkas, mendukung expression tree, dan tetap mendukung closure seperti Anonymous Method.
C#
IsEven checkEven = number => number % 2 == 0;
Ringkasnya, Lambda Expression pada dasarnya adalah evolusi dari Anonymous Method dengan sintaks yang jauh lebih ringkas dan dukungan tambahan seperti expression tree, sehingga di kode C# modern, Anonymous Method dengan kata kunci delegate sudah sangat jarang ditemui kecuali pada kode lama (legacy) yang belum dimodernisasi.
Bagian 9: Lambda Expression
Operator =>
Lambda Expression menggunakan operator => (dibaca "goes to" atau "menghasilkan") untuk memisahkan daftar parameter di sisi kiri dengan isi (body) ekspresi atau statement di sisi kanan. Diperkenalkan pada C# 3.0, Lambda Expression menjadi cara paling ringkas dan paling umum digunakan untuk membuat instance Delegate di C# modern.
C#
public delegate int Square(int number);public class Program{public static void Main(){Square square = number => number * number;Console.WriteLine(square(5)); // 25}}
Apabila parameter lebih dari satu, daftar parameter dituliskan di dalam tanda kurung; apabila hanya satu parameter, tanda kurung bersifat opsional (meskipun tetap boleh dituliskan untuk kejelasan).
C#
public delegate int Add(int a, int b);Add add = (a, b) => a + b;
Tipe data parameter juga bisa dituliskan secara eksplisit apabila diperlukan, meskipun umumnya compiler bisa menyimpulkannya sendiri (type inference) dari tipe Delegate yang dituju.
C#
Add add = (int a, int b) => a + b;
Expression Lambda
Expression Lambda adalah bentuk lambda yang isinya hanya berupa satu ekspresi tunggal, tanpa kurung kurawal maupun kata kunci return. Hasil dari ekspresi tersebut secara implisit menjadi nilai yang dikembalikan.
C#
public delegate bool IsPositive(int number);IsPositive isPositive = number => number > 0;
Expression Lambda inilah yang bisa dikonversi menjadi expression tree, yaitu representasi data dari struktur sebuah ekspresi, yang banyak dimanfaatkan oleh library seperti Entity Framework untuk menerjemahkan logika C# menjadi query SQL.
C#
using System.Linq.Expressions;Expression<Func<int, bool>> expression = number => number > 0;Console.WriteLine(expression.Body); // menampilkan struktur ekspresi, bukan hasil eksekusi
Statement Lambda
Statement Lambda adalah bentuk lambda yang isinya berupa satu atau lebih statement, dituliskan di dalam kurung kurawal, dan menggunakan kata kunci return secara eksplisit apabila Delegate-nya memiliki return value.
C#
public delegate int Categorize(int score);Categorize categorize = score =>{if (score >= 90) return 1;if (score >= 70) return 2;return 3;};Console.WriteLine(categorize(85)); // 2
Statement Lambda tidak bisa dikonversi menjadi expression tree, karena expression tree hanya mendukung representasi dari satu ekspresi tunggal, bukan rangkaian statement dengan alur kontrol seperti percabangan atau perulangan.
Capture Variable
Sama seperti Anonymous Method, Lambda Expression juga bisa mengakses (meng-capture) variabel dari lingkup di luar dirinya. Variabel yang di-capture akan tetap "hidup" dan bisa diakses oleh lambda tersebut meskipun method tempat variabel itu dideklarasikan sudah selesai dieksekusi.
C#
public class Program{public static Func<int> CreateCounter(){int count = 0;return () =>{count++;return count;};}public static void Main(){var counter = CreateCounter();Console.WriteLine(counter()); // 1Console.WriteLine(counter()); // 2Console.WriteLine(counter()); // 3}}
Pada contoh di atas, variabel count seharusnya sudah tidak ada lagi begitu method CreateCounter selesai dieksekusi, sesuai dengan aturan lifetime variabel lokal pada umumnya. Namun karena lambda yang dikembalikan meng-capture count, CLR akan memperpanjang lifetime variabel tersebut selama lambda itu masih direferensikan, dengan cara memindahkannya ke dalam objek closure yang dialokasikan di heap.
Closure
Closure adalah istilah untuk kombinasi antara sebuah fungsi (dalam hal ini, lambda) dengan lingkungan (environment) tempat variabel-variabel yang di-capture tersebut disimpan. Closure memungkinkan lambda "mengingat" nilai variabel dari lingkup luar, bahkan setelah lingkup tersebut secara normal seharusnya sudah berakhir.
Penting untuk dipahami bahwa closure meng-capture variabel, bukan nilai pada saat lambda dibuat. Ini berarti apabila variabel yang di-capture berubah setelah lambda dibuat namun sebelum lambda tersebut dipanggil, lambda akan melihat nilai yang paling terbaru, bukan nilai pada saat lambda pertama kali dideklarasikan.
C#
public class Program{public static void Main(){var actions = new List<Action>();for (int i = 0; i < 3; i++){int captured = i; // disalin ke variabel baru agar setiap lambda meng-capture nilai berbedaactions.Add(() => Console.WriteLine(captured));}foreach (var action in actions){action(); // mencetak 0, 1, 2}}}
Sejak C# 5.0, variabel iterasi pada foreach (namun bukan for konvensional) sudah secara otomatis memiliki scope baru pada setiap iterasi, sehingga closure pada foreach tidak lagi memerlukan variabel perantara seperti contoh for di atas. Namun pada for klasik seperti contoh di atas, praktik menyalin ke variabel baru per iterasi tetap relevan untuk dipahami, terutama saat membaca kode lama atau saat bekerja dengan pola perulangan manual lainnya.
Best Practice
Beberapa praktik terbaik dalam penggunaan Lambda Expression:
- Gunakan Expression Lambda untuk logika sederhana satu baris, dan Statement Lambda hanya ketika benar-benar dibutuhkan lebih dari satu statement, agar kode tetap ringkas dan mudah dibaca.
- Hindari lambda yang terlalu panjang dan kompleks, jika logika sudah melebihi beberapa baris atau memiliki banyak percabangan, pertimbangkan untuk mengekstraknya menjadi named method agar lebih mudah diuji dan dibaca.
- Berhati-hati saat meng-capture variabel dalam loop, terutama untuk memastikan setiap lambda meng-capture nilai yang dimaksud, bukan variabel yang nilainya berubah seiring iterasi berikutnya.
- Perhatikan biaya alokasi memori: setiap lambda yang meng-capture variabel dari luar akan membuat objek closure baru di heap setiap kali lambda tersebut dibuat, yang bisa berdampak pada performa apabila dilakukan berulang kali dalam hot path (dibahas lebih lanjut pada Bagian 32 mengenai Performance).
- Gunakan lambda static (static (a, b) => a + b, didukung sejak C# 9) apabila lambda tersebut tidak perlu meng-capture variabel apa pun dari luar, untuk mencegah capture yang tidak disengaja sekaligus menghindari alokasi closure yang tidak perlu.
C#
Add add = static (a, b) => a + b; // compiler akan menolak jika lambda ini mencoba meng-capture variabel luar
Bagian 10: Generic Delegate
Generic Delegate
Generic Delegate adalah Delegate yang dideklarasikan dengan satu atau lebih type parameter, sehingga tipe data yang digunakan pada parameter maupun return value-nya tidak ditentukan secara kaku saat deklarasi, melainkan ditentukan kemudian saat Delegate tersebut digunakan. Kemampuan ini diperkenalkan bersamaan dengan generic type pada C# 2.0, dan menjadi dasar bagi tipe Delegate bawaan seperti Action<T>, Func<T, TResult>, dan Predicate<T> yang akan dibahas pada Bagian 11 hingga 13.
C#
// Tanpa generic, perlu Delegate terpisah untuk setiap tipe:public delegate int ProcessInt(int value);public delegate string ProcessString(string value);public delegate double ProcessDouble(double value);
Tanpa Generic Delegate, setiap kali dibutuhkan Delegate dengan tipe data yang berbeda, developer harus mendeklarasikan Delegate baru untuk masing-masing kombinasi tipe, yang jelas tidak efisien dan sulit dikelola.
Dengan Generic Delegate, satu deklarasi saja sudah cukup untuk menangani berbagai tipe data:
C#
public delegate T Process<T>(T value);
Type Parameter
Type parameter pada Generic Delegate dituliskan di antara tanda < dan > setelah nama Delegate, dan bisa digunakan sebagai tipe untuk parameter maupun return value dari Delegate tersebut.
C#
public delegate TResult Transformer<TInput, TResult>(TInput input);public class Program{public static void Main(){Transformer<int, string> intToString = value => $"Nilai: {value}";Transformer<string, int> stringLength = value => value.Length;Console.WriteLine(intToString(100)); // Nilai: 100Console.WriteLine(stringLength("Halo")); // 4}}
Type parameter juga bisa dibatasi menggunakan generic constraint, misalnya untuk memastikan tipe yang digunakan mengimplementasikan interface tertentu atau memiliki constructor tanpa parameter.
C#
public delegate bool Comparer<T>(T a, T b) where T : IComparable<T>;public class Program{public static void Main(){Comparer<int> isGreater = (a, b) => a.CompareTo(b) > 0;Console.WriteLine(isGreater(10, 5)); // true}}
Reuse
Keuntungan utama Generic Delegate adalah kemampuannya untuk digunakan ulang (reuse) di berbagai konteks dengan tipe data yang berbeda-beda, tanpa perlu mendeklarasikan Delegate baru setiap kali kebutuhan tipe data berubah. Satu deklarasi Generic Delegate bisa dipakai untuk tipe primitif seperti int dan string, maupun untuk tipe kompleks seperti class atau record buatan sendiri.
C#
public delegate bool Validator<T>(T item);public class Employee{public string Name { get; set; } = string.Empty;public decimal Salary { get; set; }}public class Program{public static bool IsPositive(int number) => number > 0;public static bool HasValidSalary(Employee employee) => employee.Salary > 0;public static void Main(){Validator<int> numberValidator = IsPositive;Validator<Employee> employeeValidator = HasValidSalary;Console.WriteLine(numberValidator(5));Console.WriteLine(employeeValidator(new Employee { Name = "Budi", Salary = 3000000 }));}}
Contoh Generic Processor
Sebagai contoh yang lebih lengkap, berikut sebuah "Generic Processor" yang memanfaatkan Generic Delegate untuk memproses sebuah nilai melalui rangkaian langkah (pipeline) yang bisa berbeda-beda tergantung tipe data dan kebutuhan pemanggilnya.
C#
public delegate TOutput Processor<TInput, TOutput>(TInput input);public class DataPipeline<TInput, TOutput>{private readonly Processor<TInput, TOutput> _process;public DataPipeline(Processor<TInput, TOutput> process){_process = process;}public TOutput Run(TInput input) => _process(input);}public class Program{public static void Main(){var upperCasePipeline = new DataPipeline<string, string>(input => input.ToUpper());Console.WriteLine(upperCasePipeline.Run("halo dunia")); // HALO DUNIAvar squarePipeline = new DataPipeline<int, int>(input => input * input);Console.WriteLine(squarePipeline.Run(6)); // 36var summaryPipeline = new DataPipeline<List<int>, string>(numbers => $"Total: {numbers.Sum()}, Rata-rata: {numbers.Average():F2}");Console.WriteLine(summaryPipeline.Run(new List<int> { 10, 20, 30 })); // Total: 60, Rata-rata: 20.00}}
Contoh DataPipeline<TInput, TOutput> di atas menunjukkan bagaimana Generic Delegate memungkinkan satu class yang sama digunakan untuk berbagai kombinasi tipe input dan output, tanpa perlu menulis ulang logika pipeline untuk setiap jenis data yang berbeda. Pendekatan semacam ini menjadi fondasi penting yang nantinya akan terlihat jelas manfaatnya saat membahas Func, Action, dan Predicate pada bagian-bagian selanjutnya, yang pada dasarnya adalah kumpulan Generic Delegate siap pakai yang sudah disediakan oleh .NET.
Bagian 11: Action
Action
Action adalah salah satu Generic Delegate bawaan yang disediakan oleh .NET dalam namespace System, digunakan untuk merepresentasikan sebuah method yang tidak mengembalikan nilai (return type-nya selalu void). Kehadiran Action dan keluarganya menghilangkan kebutuhan untuk mendeklarasikan Delegate custom secara manual pada kebanyakan kasus umum, karena Action sudah mencakup berbagai kombinasi jumlah parameter yang mungkin dibutuhkan.
C#
public class Program{public static void Main(){Action greet = () => Console.WriteLine("Halo!");greet();}}
Versi paling sederhana, Action tanpa type parameter, digunakan untuk method tanpa parameter sama sekali.
Action<T>
Action<T> digunakan untuk method yang menerima satu parameter bertipe T dan tidak mengembalikan nilai.
C#
public class Program{public static void Main(){Action<string> print = message => Console.WriteLine(message);print("Data berhasil disimpan.");}}
Action<T1, T2>
Action<T1, T2> menerima dua parameter dengan tipe yang bisa berbeda satu sama lain.
C#
public class Program{public static void Main(){Action<string, int> logWithLevel = (message, level) =>{Console.WriteLine($"[Level {level}] {message}");};logWithLevel("Koneksi database terputus", 3);}}
Hingga Action<T1...T16>
.NET menyediakan varian Action dari tanpa parameter hingga maksimal 16 parameter (Action<T1, T2, ..., T16>), sehingga hampir seluruh kebutuhan jumlah parameter dalam praktik sehari-hari sudah tercakup tanpa perlu mendeklarasikan Delegate custom.
C#
public class Program{public static void Main(){Action<string, string, decimal, int, DateTime> recordTransaction =(accountNumber, transactionType, amount, referenceId, timestamp) =>{Console.WriteLine($"Rekening {accountNumber} - {transactionType} sebesar {amount} " +$"(Ref: {referenceId}) pada {timestamp:yyyy-MM-dd HH:mm}");};recordTransaction("1234567890", "TRANSFER", 500000, 90001, DateTime.Now);}}
Meskipun Action mendukung hingga 16 parameter, dalam praktiknya method dengan jumlah parameter sebanyak itu jarang ditemui dan justru menjadi indikasi bahwa desain method tersebut perlu dipertimbangkan ulang, misalnya dengan mengelompokkan parameter-parameter terkait ke dalam sebuah class atau record tersendiri.
Contoh: Logger
C#
public class Program{public static void Main(){Action<string> logInfo = message => Console.WriteLine($"[INFO] {message}");Action<string> logError = message => Console.WriteLine($"[ERROR] {message}");logInfo("Aplikasi dimulai.");logError("Gagal terhubung ke database.");}}
Contoh: Notification
C#
public class NotificationService{public void Notify(string recipient, Action<string> sendMethod){sendMethod(recipient);}}public class Program{public static void Main(){var service = new NotificationService();service.Notify("budi@example.com", recipient =>Console.WriteLine($"Mengirim email ke {recipient}"));service.Notify("08123456789", recipient =>Console.WriteLine($"Mengirim SMS ke {recipient}"));}}
Contoh: Repository
C#
public class Employee{public int Id { get; set; }public string Name { get; set; } = string.Empty;}public class EmployeeRepository{private readonly List<Employee> _employees = new();public void Add(Employee employee, Action<Employee> onAdded){_employees.Add(employee);onAdded(employee);}}public class Program{public static void Main(){var repository = new EmployeeRepository();repository.Add(new Employee { Id = 1, Name = "Budi" },employee => Console.WriteLine($"Employee {employee.Name} berhasil ditambahkan dengan Id {employee.Id}"));}}
Contoh: Middleware
C#
public class RequestContext{public string Path { get; set; } = string.Empty;public bool IsAuthenticated { get; set; }}public class Program{public static void RunMiddleware(RequestContext context, Action<RequestContext> middleware){Console.WriteLine($"Memproses request: {context.Path}");middleware(context);Console.WriteLine("Request selesai diproses.");}public static void Main(){var context = new RequestContext { Path = "/api/orders", IsAuthenticated = true };RunMiddleware(context, ctx =>{if (!ctx.IsAuthenticated){Console.WriteLine("Akses ditolak.");return;}Console.WriteLine($"Akses diberikan untuk {ctx.Path}");});}}
Bagian 12: Func
Func
Func adalah keluarga Generic Delegate bawaan .NET yang digunakan untuk merepresentasikan method yang mengembalikan nilai. Berbeda dengan Action yang seluruh type parameter-nya merepresentasikan parameter input, pada Func type parameter terakhir selalu merepresentasikan tipe dari return value, sedangkan type parameter sebelumnya merepresentasikan parameter input.
C#
public class Program{public static void Main(){Func<int> getRandomNumber = () => new Random().Next(1, 100);Console.WriteLine(getRandomNumber());}}
Pada contoh di atas, Func<int> berarti method tanpa parameter input yang mengembalikan nilai bertipe int.
Return Type
Return type pada Func selalu berada di posisi type parameter paling akhir. Sebagai contoh, Func<string, int> berarti method yang menerima satu parameter bertipe string dan mengembalikan nilai bertipe int, sedangkan Func<int, int, bool> berarti method yang menerima dua parameter bertipe int dan mengembalikan nilai bertipe bool.
C#
public class Program{public static void Main(){Func<string, int> getLength = text => text.Length;Console.WriteLine(getLength("Halo Dunia")); // 10Func<int, int, bool> isDivisible = (a, b) => a % b == 0;Console.WriteLine(isDivisible(10, 2)); // true}}
Sama seperti Action, Func juga tersedia dalam berbagai varian jumlah parameter input, dari Func<TResult> (tanpa parameter) hingga Func<T1, T2, ..., T16, TResult> (16 parameter input ditambah satu return type).
Input Parameter
Parameter input pada Func dituliskan berurutan sebelum return type, dan bisa berupa tipe data apa pun, termasuk tipe generic lain, class, atau bahkan Func itu sendiri sebagai parameter (higher order function tingkat lanjut).
C#
public class Program{public static int Apply(int value, Func<int, int> transformer){return transformer(value);}public static void Main(){int result = Apply(5, x => x * x);Console.WriteLine(result); // 25}}
Contoh Func yang menerima Func lain sebagai parameter, sekaligus mengembalikan Func baru:
C#
public class Program{public static Func<int, int> Compose(Func<int, int> first, Func<int, int> second){return value => second(first(value));}public static void Main(){Func<int, int> addOne = x => x + 1;Func<int, int> square = x => x * x;Func<int, int> addThenSquare = Compose(addOne, square);Console.WriteLine(addThenSquare(4)); // (4 + 1) ^ 2 = 25}}
Func vs Method
Perbedaan mendasar antara Func dan method konvensional terletak pada fleksibilitasnya: method biasa terikat pada nama dan hanya bisa dipanggil melalui nama tersebut, sedangkan Func bisa disimpan dalam variabel, dikirim sebagai parameter, diganti-ganti isinya pada saat runtime, dan dikembalikan sebagai hasil dari method lain.
C#
public class Program{// Method konvensional: hanya bisa dipanggil melalui nama "CalculateTax"public static decimal CalculateTax(decimal amount) => amount * 0.11m;public static void Main(){// Func: bisa diganti-ganti isinya secara dinamisFunc<decimal, decimal> taxCalculator = CalculateTax;if (DateTime.Now.Year >= 2025){taxCalculator = amount => amount * 0.12m; // aturan pajak baru}Console.WriteLine(taxCalculator(1000000));}}
Method konvensional lebih tepat digunakan untuk logika inti yang bersifat tetap dan menjadi bagian dari kontrak sebuah class, sedangkan Func lebih tepat digunakan ketika perilaku tersebut perlu bisa berubah-ubah tergantung konteks pemanggilan, atau ketika perilaku itu perlu disuntikkan dari luar (lihat kembali pembahasan Dependency Injection sederhana pada Bagian 7).
Contoh: Calculator
C#
public class Program{public static void Main(){Func<int, int, int> add = (a, b) => a + b;Func<int, int, int> subtract = (a, b) => a - b;Func<int, int, int> multiply = (a, b) => a * b;Console.WriteLine(add(10, 5)); // 15Console.WriteLine(subtract(10, 5)); // 5Console.WriteLine(multiply(10, 5)); // 50}}
Contoh: Discount Engine
C#
public class Program{public static decimal ApplyDiscount(decimal price, Func<decimal, decimal> discountRule){return discountRule(price);}public static void Main(){Func<decimal, decimal> memberDiscount = price => price * 0.9m;Func<decimal, decimal> flashSaleDiscount = price => price - 50000;Console.WriteLine(ApplyDiscount(500000, memberDiscount)); // 450000Console.WriteLine(ApplyDiscount(500000, flashSaleDiscount)); // 450000}}
Contoh: Tax Engine
C#
public class Program{public static Dictionary<string, Func<decimal, decimal>> TaxRules = new(){["domestic"] = amount => amount * 0.11m,["export"] = amount => 0m,["luxury"] = amount => amount * 0.2m};public static void Main(){decimal amount = 10000000;foreach (var rule in TaxRules){decimal tax = rule.Value(amount);Console.WriteLine($"Pajak untuk kategori {rule.Key}: {tax}");}}}
Bagian 13: Predicate
Predicate<T>
Predicate<T> adalah Generic Delegate bawaan .NET yang secara khusus digunakan untuk merepresentasikan method yang menerima satu parameter bertipe T dan selalu mengembalikan nilai bertipe bool. Secara fungsional, Predicate<T> sebenarnya setara dengan Func<T, bool> perbedaannya murni pada penamaan yang lebih ekspresif untuk konteks pengujian suatu kondisi terhadap sebuah nilai.
C#
public class Program{public static void Main(){Predicate<int> isEven = number => number % 2 == 0;Console.WriteLine(isEven(4)); // trueConsole.WriteLine(isEven(7)); // false}}
Karena Predicate<T> dan Func<T, bool> memiliki signature yang identik, sebuah lambda yang sama bisa ditugaskan ke keduanya, namun keduanya tetap dianggap sebagai dua tipe Delegate yang berbeda oleh compiler (sama seperti dibahas pada Bagian 3 mengenai Delegate Type), sehingga tidak bisa saling ditukar secara langsung tanpa konversi.
C#
Predicate<int> isPositive = number => number > 0;Func<int, bool> isPositiveFunc = number => number > 0;// Baris di bawah ini tidak valid meskipun signature-nya identik:// Func<int, bool> convertedFunc = isPositive;// Harus dikonversi secara eksplisit:Func<int, bool> convertedFunc = new Func<int, bool>(isPositive);
Boolean Delegate
Predicate<T> sering disebut sebagai "Boolean Delegate" karena tujuannya yang spesifik: menguji apakah suatu nilai memenuhi kondisi tertentu, lalu mengembalikan jawaban true atau false. Penggunaan Predicate<T> membuat kode lebih ekspresif dibandingkan menggunakan Func<T, bool> yang lebih generik, terutama pada API milik List<T> yang memang didesain sejak awal untuk menerima Predicate<T>.
C#
public class Program{public static void Main(){Predicate<string> isValidUsername = username =>username.Length >= 5 && !username.Contains(" ");Console.WriteLine(isValidUsername("haekal_dev")); // trueConsole.WriteLine(isValidUsername("ab")); // false}}
Filtering
Salah satu kegunaan paling umum dari Predicate<T> adalah untuk keperluan filtering, yaitu memilih elemen-elemen dari sebuah koleksi yang memenuhi kondisi tertentu. Method List<T>.FindAll misalnya menerima Predicate<T> untuk menentukan elemen mana saja yang harus disertakan dalam hasil.
C#
public class Program{public static void Main(){var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };List<int> evenNumbers = numbers.FindAll(number => number % 2 == 0);Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6, 8, 10}}
Validation
Predicate<T> juga sangat umum digunakan untuk keperluan validasi, baik validasi terhadap satu objek tunggal maupun validasi terhadap seluruh elemen dalam sebuah koleksi.
C#
public class Program{public static bool Validate<T>(T item, Predicate<T> rule, string errorMessage){if (!rule(item)){Console.WriteLine($"Validasi gagal: {errorMessage}");return false;}return true;}public static void Main(){int stock = -5;Validate(stock, value => value >= 0, "Stok tidak boleh bernilai negatif.");}}
Contoh: Employee
C#
public class Employee{public string Name { get; set; } = string.Empty;public int YearsOfService { get; set; }}public class Program{public static void Main(){var employees = new List<Employee>{new() { Name = "Budi", YearsOfService = 3 },new() { Name = "Dewi", YearsOfService = 7 },new() { Name = "Rian", YearsOfService = 1 }};Predicate<Employee> isEligibleForBonus = employee => employee.YearsOfService >= 3;List<Employee> eligibleEmployees = employees.FindAll(isEligibleForBonus);foreach (var employee in eligibleEmployees){Console.WriteLine($"{employee.Name} berhak menerima bonus.");}}}
Contoh: Product
C#
public class Product{public string Name { get; set; } = string.Empty;public int Stock { get; set; }public decimal Price { get; set; }}public class Program{public static void Main(){var products = new List<Product>{new() { Name = "Keyboard", Stock = 0, Price = 350000 },new() { Name = "Mouse", Stock = 15, Price = 120000 },new() { Name = "Monitor", Stock = 3, Price = 2500000 }};Predicate<Product> isOutOfStock = product => product.Stock == 0;Product? outOfStockProduct = products.Find(isOutOfStock);Console.WriteLine(outOfStockProduct is not null? $"Produk habis: {outOfStockProduct.Name}": "Semua produk masih tersedia.");}}
Contoh: Student
C#
public class Student{public string Name { get; set; } = string.Empty;public double Score { get; set; }}public class Program{public static void Main(){var students = new List<Student>{new() { Name = "Gisa", Score = 88 },new() { Name = "Budi", Score = 65 },new() { Name = "Tono", Score = 92 }};Predicate<Student> isPassing = student => student.Score >= 70;bool allPassing = students.TrueForAll(isPassing);Console.WriteLine($"Semua siswa lulus: {allPassing}");int removedCount = students.RemoveAll(student => !isPassing(student));Console.WriteLine($"Jumlah siswa yang tidak lulus dan dihapus dari daftar: {removedCount}");}}
Predicate dan LINQ
Meskipun Predicate<T> banyak dipakai pada method bawaan List<T>, LINQ (dibahas lebih lanjut pada Bagian 16) sebenarnya tidak menggunakan Predicate<T> secara langsung, melainkan menggunakan Func<T, bool> pada operator seperti Where. Ini menjadi salah satu alasan mengapa penting untuk memahami bahwa Predicate<T> dan Func<T, bool>, meskipun setara secara konsep, tetap merupakan dua tipe Delegate yang berbeda dan tidak selalu bisa dipertukarkan secara langsung antara API List<T> dan API LINQ.
C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };// Predicate<T>, dipakai oleh method milik List<T>List<int> evenUsingList = numbers.FindAll(n => n % 2 == 0);// Func<T, bool>, dipakai oleh LINQIEnumerable<int> evenUsingLinq = numbers.Where(n => n % 2 == 0);
Bagian 14: Comparison Delegate
Comparison<T>
Comparison<T> adalah Generic Delegate bawaan .NET yang secara khusus digunakan untuk membandingkan dua nilai bertipe T, dan mengembalikan nilai int sebagai hasil perbandingannya. Comparison<T> memiliki signature yang identik dengan method CompareTo pada interface IComparable<T>, namun dalam bentuk Delegate, sehingga logika perbandingan bisa ditentukan secara dinamis tanpa harus mengimplementasikan interface pada class yang dibandingkan.
C#
public delegate int Comparison<in T>(T x, T y);
Aturan nilai kembalian Comparison<T> mengikuti konvensi yang sama seperti CompareTo pada umumnya:
- Nilai negatif apabila x dianggap lebih kecil (mendahului) y.
- Nilai nol apabila x dan y dianggap setara.
- Nilai positif apabila x dianggap lebih besar (setelah) y.
C#
public class Program{public static void Main(){Comparison<int> compareAscending = (x, y) => x.CompareTo(y);Console.WriteLine(compareAscending(3, 5)); // negatif, karena 3 < 5Console.WriteLine(compareAscending(5, 5)); // 0, karena setaraConsole.WriteLine(compareAscending(8, 5)); // positif, karena 8 > 5}}
Sorting
Comparison<T> paling umum digunakan untuk keperluan pengurutan (sorting) data, terutama pada koleksi yang perlu diurutkan berdasarkan aturan yang bisa berubah-ubah tergantung konteks, tanpa perlu mengubah struktur class dari data itu sendiri.
C#
public class Product{public string Name { get; set; } = string.Empty;public decimal Price { get; set; }}public class Program{public static void Main(){var products = new List<Product>{new() { Name = "Keyboard", Price = 350000 },new() { Name = "Mouse", Price = 120000 },new() { Name = "Monitor", Price = 2500000 }};Comparison<Product> byPriceAscending = (a, b) => a.Price.CompareTo(b.Price);products.Sort(byPriceAscending);foreach (var product in products){Console.WriteLine($"{product.Name}: {product.Price}");}// Output berurutan dari harga termurah: Mouse, Keyboard, Monitor}}
Untuk mengurutkan secara descending, cukup membalikkan urutan parameter yang dibandingkan, atau mengalikan hasilnya dengan -1.
C#
Comparison<Product> byPriceDescending = (a, b) => b.Price.CompareTo(a.Price);products.Sort(byPriceDescending);
Custom Comparer
Selain digunakan langsung sebagai Delegate, logika perbandingan yang sama juga bisa dibungkus menjadi sebuah custom comparer dengan mengimplementasikan interface IComparer<T>, yang kemudian bisa digunakan pada method-method lain yang menerima IComparer<T>, seperti OrderBy pada LINQ atau SortedList<TKey, TValue>.
C#
public class ProductPriceComparer : IComparer<Product>{public int Compare(Product? x, Product? y){if (x is null || y is null) return 0;return x.Price.CompareTo(y.Price);}}public class Program{public static void Main(){var products = new List<Product>{new() { Name = "Keyboard", Price = 350000 },new() { Name = "Mouse", Price = 120000 },new() { Name = "Monitor", Price = 2500000 }};products.Sort(new ProductPriceComparer());foreach (var product in products){Console.WriteLine($"{product.Name}: {product.Price}");}}}
Dibandingkan dengan Comparison<T> sebagai Delegate, pendekatan IComparer<T> lebih tepat digunakan apabila logika perbandingan tersebut cukup kompleks, perlu digunakan berulang kali di banyak tempat, atau perlu diuji secara terpisah; sedangkan Comparison<T> lebih tepat untuk kebutuhan pengurutan yang bersifat sekali pakai (ad-hoc) dan cukup sederhana untuk dituliskan langsung sebagai lambda.
List.Sort()
Method List<T>.Sort() menyediakan beberapa overload yang menerima berbagai bentuk logika perbandingan, termasuk Comparison<T> secara langsung maupun IComparer<T>.
C#
var numbers = new List<int> { 5, 2, 8, 1, 9, 3 };// Overload tanpa parameter: menggunakan default comparer (ascending untuk tipe primitif)numbers.Sort();// Overload dengan Comparison<T>numbers.Sort((a, b) => b.CompareTo(a)); // descending// Overload dengan IComparer<T>numbers.Sort(Comparer<int>.Create((a, b) => a.CompareTo(b)));
Perlu diperhatikan bahwa List<T>.Sort() mengurutkan data secara langsung di tempat (in-place), berbeda dengan OrderBy pada LINQ yang akan dibahas pada Bagian 16, di mana OrderBy justru menghasilkan koleksi baru tanpa mengubah koleksi aslinya.
C#
var original = new List<int> { 5, 2, 8, 1, 9 };original.Sort(); // original langsung berubah menjadi { 1, 2, 5, 8, 9 }var another = new List<int> { 5, 2, 8, 1, 9 };var sorted = another.OrderBy(n => n).ToList(); // another tetap { 5, 2, 8, 1, 9 }, sorted adalah koleksi baru
Bagian 15: Delegate pada Collection
List<T>
Kelas List<T> di .NET menyediakan berbagai method bawaan yang menerima Delegate sebagai parameter, terutama Predicate<T> dan Comparison<T>, untuk melakukan operasi seperti pencarian, validasi, pengurutan, dan transformasi terhadap elemen-elemen di dalamnya. Bagian ini merangkum method-method tersebut sebagai satu kesatuan pembahasan, karena seluruhnya bekerja dengan cara yang serupa: menerima Delegate, lalu menjalankannya terhadap setiap elemen dalam koleksi.
Sebagai data contoh yang akan dipakai berulang kali pada bagian ini:
C#
public class Product{public string Name { get; set; } = string.Empty;public decimal Price { get; set; }public int Stock { get; set; }}var products = new List<Product>{new() { Name = "Keyboard", Price = 350000, Stock = 12 },new() { Name = "Mouse", Price = 120000, Stock = 0 },new() { Name = "Monitor", Price = 2500000, Stock = 5 },new() { Name = "Webcam", Price = 450000, Stock = 0 }};
Find
Method Find mengembalikan elemen pertama dalam koleksi yang memenuhi kondisi Predicate<T> yang diberikan, atau nilai default (misalnya null untuk reference type) apabila tidak ada elemen yang cocok.
C#
Product? outOfStock = products.Find(product => product.Stock == 0);Console.WriteLine(outOfStock?.Name); // Mouse (elemen pertama yang stoknya habis)
Exists
Method Exists mengembalikan nilai bool yang menunjukkan apakah ada setidaknya satu elemen dalam koleksi yang memenuhi kondisi Predicate<T> yang diberikan, tanpa perlu mengembalikan elemennya secara langsung.
C#
bool hasExpensiveProduct = products.Exists(product => product.Price > 2000000);Console.WriteLine(hasExpensiveProduct); // true, karena Monitor harganya di atas 2 juta
TrueForAll
Method TrueForAll mengembalikan nilai bool yang menunjukkan apakah seluruh elemen dalam koleksi memenuhi kondisi Predicate<T> yang diberikan. Apabila koleksi kosong, method ini akan selalu mengembalikan true.
C#
bool allInStock = products.TrueForAll(product => product.Stock > 0);Console.WriteLine(allInStock); // false, karena ada Mouse dan Webcam yang stoknya 0
RemoveAll
Method RemoveAll menghapus seluruh elemen dalam koleksi yang memenuhi kondisi Predicate<T> yang diberikan, dan mengembalikan jumlah elemen yang berhasil dihapus. Operasi ini mengubah koleksi asli secara langsung (in-place).
C#
int removedCount = products.RemoveAll(product => product.Stock == 0);Console.WriteLine($"{removedCount} produk dengan stok habis telah dihapus.");// products sekarang hanya berisi Keyboard dan Monitor
ConvertAll
Method ConvertAll mengubah setiap elemen dalam koleksi menjadi tipe lain menggunakan Converter<TInput, TOutput>, sebuah Generic Delegate bawaan .NET yang secara fungsional setara dengan Func<TInput, TOutput>, lalu mengembalikan koleksi baru berisi hasil konversi tersebut.
C#
List<string> productNames = products.ConvertAll(product => product.Name);Console.WriteLine(string.Join(", ", productNames));// Keyboard, Mouse, Monitor, WebcamList<decimal> discountedPrices = products.ConvertAll(product => product.Price * 0.9m);Console.WriteLine(string.Join(", ", discountedPrices));
Perlu dicatat bahwa koleksi asli (products) tidak berubah sama sekali setelah pemanggilan ConvertAll, karena method ini menghasilkan List<TOutput> yang benar-benar baru.
Sort
Sebagaimana sudah dibahas secara lebih mendalam pada Bagian 14, method Sort menerima Comparison<T> (atau IComparer<T>) untuk menentukan urutan elemen dalam koleksi, dan bekerja secara in-place terhadap koleksi aslinya.
C#
products.Sort((a, b) => a.Price.CompareTo(b.Price));foreach (var product in products){Console.WriteLine($"{product.Name}: {product.Price}");}
Ringkasan Method dan Delegate yang Digunakan
Seluruh method di atas menunjukkan bagaimana Delegate memungkinkan List<T> menyediakan API yang sangat fleksibel: perilaku pencarian, validasi, transformasi, maupun pengurutan sepenuhnya bisa disesuaikan oleh pemanggilnya, tanpa List<T> itu sendiri perlu mengetahui aturan bisnis spesifik yang berlaku pada tipe data yang disimpannya. Pola desain semacam ini akan terlihat semakin luas cakupannya saat membahas LINQ pada Bagian 16, yang pada dasarnya memperluas pendekatan yang sama ke hampir seluruh jenis koleksi di .NET, tidak hanya List<T>.
| Method | Tipe Delegate | Fungsi |
|---|---|---|
| Find | Predicate<T> | Mencari elemen pertama yang cocok |
| Exists | Predicate<T> | Memeriksa apakah ada elemen yang cocok |
| TrueForAll | Predicate<T> | Memeriksa apakah seluruh elemen cocok |
| RemoveAll | Predicate<T> | Menghapus seluruh elemen yang cocok |
| ConvertAll | Converter<TInput, TOutput> | Mengubah setiap elemen menjadi koleksi baru bertipe lain |
| Sort | Comparison<T> | Mengurutkan elemen berdasarkan aturan tertentu |
Bagian 16: Delegate dan LINQ
LINQ (Language Integrated Query) adalah salah satu fitur C# yang paling bergantung pada Delegate, khususnya Func<T, TResult>. Hampir seluruh operator LINQ menerima satu atau lebih Delegate sebagai parameter untuk menentukan bagaimana data harus difilter, diubah, diurutkan, atau dikelompokkan. Bagian ini membahas operator-operator LINQ yang paling umum digunakan dari sudut pandang Delegate yang mendasarinya.
Sebagai data contoh yang akan dipakai berulang kali pada bagian ini:
C#
public class Employee{public string Name { get; set; } = string.Empty;public string Department { get; set; } = string.Empty;public decimal Salary { get; set; }}var employees = new List<Employee>{new() { Name = "Budi", Department = "Engineering", Salary = 12000000 },new() { Name = "Dewi", Department = "Engineering", Salary = 15000000 },new() { Name = "Anton", Department = "Marketing", Salary = 9000000 },new() { Name = "Gisa", Department = "Marketing", Salary = 10000000 }};
Where
Operator Where menerima Func<T, bool> untuk memfilter elemen, mirip dengan FindAll pada List<T>, namun bersifat lazy (dieksekusi hanya ketika hasilnya benar-benar dienumerasi) dan bekerja untuk seluruh tipe IEnumerable<T>, bukan hanya List<T>.
C#
var engineeringEmployees = employees.Where(employee => employee.Department == "Engineering");foreach (var employee in engineeringEmployees){Console.WriteLine(employee.Name);}
Select
Operator Select menerima Func<T, TResult> untuk mengubah (transform) setiap elemen menjadi bentuk lain, mirip dengan ConvertAll pada List<T>, namun juga bersifat lazy dan berlaku untuk IEnumerable<T> secara umum.
C#
var employeeNames = employees.Select(employee => employee.Name);Console.WriteLine(string.Join(", ", employeeNames));var salarySummaries = employees.Select(employee => $"{employee.Name}: {employee.Salary:C0}");
OrderBy
Operator OrderBy menerima Func<T, TKey> untuk menentukan berdasarkan properti apa data harus diurutkan, berbeda dengan Comparison<T> pada List<T>.Sort() yang menerima logika perbandingan penuh. OrderBy mengurutkan secara ascending, sedangkan OrderByDescending untuk descending.
C#
var orderedBySalary = employees.OrderBy(employee => employee.Salary);var orderedBySalaryDesc = employees.OrderByDescending(employee => employee.Salary);foreach (var employee in orderedBySalary){Console.WriteLine($"{employee.Name}: {employee.Salary}");}
Pengurutan berdasarkan lebih dari satu kriteria dilakukan dengan menambahkan ThenBy atau ThenByDescending setelah OrderBy.
C#
var orderedByDeptThenSalary = employees.OrderBy(employee => employee.Department).ThenByDescending(employee => employee.Salary);
GroupBy
Operator GroupBy menerima Func<T, TKey> untuk mengelompokkan elemen berdasarkan nilai kunci tertentu, menghasilkan koleksi berisi grup-grup (IGrouping<TKey, T>), di mana setiap grup memiliki kunci (Key) dan kumpulan elemen yang termasuk di dalamnya.
C#
var groupedByDepartment = employees.GroupBy(employee => employee.Department);foreach (var group in groupedByDepartment){Console.WriteLine($"Departemen: {group.Key}");foreach (var employee in group){Console.WriteLine($" - {employee.Name}");}}
Any
Operator Any menerima Func<T, bool> (opsional) dan mengembalikan bool yang menunjukkan apakah ada elemen yang memenuhi kondisi tersebut, setara dengan Exists pada List<T>. Tanpa parameter, Any hanya memeriksa apakah koleksi memiliki elemen sama sekali.
C#
bool hasHighEarner = employees.Any(employee => employee.Salary > 14000000);Console.WriteLine(hasHighEarner); // truebool hasAnyEmployee = employees.Any();Console.WriteLine(hasAnyEmployee); // true
All
Operator All menerima Func<T, bool> dan mengembalikan bool yang menunjukkan apakah seluruh elemen memenuhi kondisi tersebut, setara dengan TrueForAll pada List<T>.
C#
bool allEarnAboveMinimum = employees.All(employee => employee.Salary >= 5000000);Console.WriteLine(allEarnAboveMinimum); // true
First dan Single
Operator First mengembalikan elemen pertama yang memenuhi kondisi Func<T, bool> (atau elemen pertama dalam koleksi apabila tanpa parameter), dan akan melempar exception apabila tidak ada elemen yang cocok. FirstOrDefault adalah versi yang lebih aman, mengembalikan nilai default alih-alih melempar exception.
C#
var firstEngineer = employees.First(employee => employee.Department == "Engineering");var firstEngineerOrDefault = employees.FirstOrDefault(employee => employee.Department == "Finance"); // null, tidak melempar exception
Operator Single mirip dengan First, namun mensyaratkan bahwa tepat satu elemen yang cocok dengan kondisi, apabila tidak ada atau lebih dari satu elemen yang cocok, Single akan melempar exception. SingleOrDefault adalah versi yang lebih toleran terhadap kasus tidak ditemukannya elemen, namun tetap akan melempar exception apabila ditemukan lebih dari satu elemen yang cocok.
C#
var uniqueHighEarner = employees.Single(employee => employee.Salary == 15000000);
Projection
Projection adalah istilah umum untuk proses mengubah bentuk data dari satu struktur ke struktur lain, biasanya melalui Select, dan sering dikombinasikan dengan anonymous type atau record untuk menghasilkan bentuk data yang lebih ringkas sesuai kebutuhan.
C#
var employeeSummaries = employees.Select(employee => new{employee.Name,IsHighEarner = employee.Salary > 10000000});foreach (var summary in employeeSummaries){Console.WriteLine($"{summary.Name} - High Earner: {summary.IsHighEarner}");}
Projection juga bisa dikombinasikan dengan operator lain seperti Where dan OrderBy dalam satu rangkaian (chaining), yang menunjukkan bagaimana berbagai Delegate yang berbeda bisa dikombinasikan untuk membentuk satu alur query yang deklaratif dan mudah dibaca.
C#
var result = employees.Where(employee => employee.Department == "Engineering").OrderByDescending(employee => employee.Salary).Select(employee => $"{employee.Name}: {employee.Salary:C0}");foreach (var line in result){Console.WriteLine(line);}
Rangkaian query di atas menunjukkan kekuatan utama LINQ: setiap operator menerima Delegate sebagai "aturan" yang bisa dikombinasikan secara bebas, sehingga satu rangkaian query yang kompleks bisa dibangun dari operator-operator sederhana yang masing-masing hanya bertanggung jawab atas satu tugas spesifik saja.
Bagian 17: Callback Pattern
Pada Bagian 7 sudah disinggung sekilas mengenai Callback Pattern sebagai salah satu bentuk penggunaan Delegate sebagai parameter. Bagian ini membahas Callback Pattern secara lebih mendalam, mencakup perbedaan antara callback sinkron dan asinkron, serta bagaimana callback bisa dirangkai menjadi sebuah pipeline pemrosesan data.
Sync Callback
Sync Callback (callback sinkron) adalah bentuk callback paling dasar, di mana Delegate yang diberikan sebagai parameter dipanggil secara langsung dan berurutan sebagai bagian dari alur eksekusi method yang menerimanya, tanpa melibatkan thread terpisah maupun operasi asynchronous.
C#
public delegate void ProgressCallback(int percentage);public class FileProcessor{public void Process(string fileName, ProgressCallback onProgress){for (int i = 0; i <= 100; i += 20){// simulasi proses yang berjalan secara sinkronSystem.Threading.Thread.Sleep(100);onProgress(i);}}}public class Program{public static void Main(){var processor = new FileProcessor();processor.Process("data.csv", percentage =>{Console.WriteLine($"Memproses {percentage}%...");});Console.WriteLine("Proses selesai."); // baris ini baru tercetak setelah seluruh callback selesai dipanggil}}
Karena bersifat sinkron, kode pemanggil (Main pada contoh di atas) akan menunggu hingga seluruh proses di dalam Process (termasuk seluruh pemanggilan callback) selesai sebelum melanjutkan ke baris berikutnya.
Async Callback
Async Callback adalah callback yang dipanggil sebagai bagian dari operasi yang berjalan secara asynchronous, biasanya dalam konteks async/await atau Task. Berbeda dengan Sync Callback, kode pemanggil tidak perlu menunggu secara blocking, namun callback akan dipanggil kapan pun operasi asynchronous tersebut mencapai titik tertentu.
C#
public delegate void DownloadProgressCallback(int percentage);public class Downloader{public async Task DownloadAsync(string url, DownloadProgressCallback onProgress){for (int i = 0; i <= 100; i += 25){await Task.Delay(200); // simulasi operasi I/O asynchronousonProgress(i);}}}public class Program{public static async Task Main(){var downloader = new Downloader();Console.WriteLine("Mulai download...");await downloader.DownloadAsync("https://example.com/file.zip", percentage =>{Console.WriteLine($"Progress: {percentage}%");});Console.WriteLine("Download selesai.");}}
Perbedaan mendasar antara Sync Callback dan Async Callback bukan terletak pada Delegate itu sendiri (keduanya sama-sama bisa memakai Action atau Delegate custom), melainkan pada konteks eksekusi di mana callback tersebut dipanggil: apakah dipanggil secara langsung dalam alur sinkron, atau dipanggil di sela-sela operasi await yang bersifat non-blocking. Pembahasan yang lebih mendalam mengenai Delegate dengan async/await akan dibahas pada Bagian 24.
Pipeline
Pipeline adalah pola di mana data diproses melalui serangkaian tahapan berurutan, dan setiap tahapan direpresentasikan sebagai sebuah Delegate. Pola ini memungkinkan tahapan-tahapan pemrosesan disusun, diganti, atau ditambahkan secara fleksibel tanpa mengubah struktur inti dari pipeline itu sendiri.
Pipeline berbasis Delegate juga bisa dikombinasikan dengan callback untuk memberi tahu progres di setiap tahapan, sebagaimana ditunjukkan pada contoh Downloader dan Uploader berikut.
C#
public class Program{public static void RunPipeline<T>(T input, params Func<T, T>[] steps){T result = input;foreach (var step in steps){result = step(result);}Console.WriteLine(result);}public static void Main(){RunPipeline(" Halo Dunia ",input => input.Trim(),input => input.ToLower(),input => input.Replace(" ", "-"));// Output: halo-dunia}}
Contoh: Downloader
C#
public delegate void ProgressCallback(string stage, int percentage);public class Downloader{public void Download(string url, ProgressCallback onProgress){onProgress("Menghubungkan ke server", 10);onProgress("Mengunduh data", 60);onProgress("Memverifikasi file", 90);onProgress("Selesai", 100);}}public class Program{public static void Main(){var downloader = new Downloader();downloader.Download("https://example.com/report.pdf", (stage, percentage) =>{Console.WriteLine($"[{percentage}%] {stage}");});}}
Contoh: Uploader
C#
public delegate void UploadCallback(string fileName, bool success);public class Uploader{public void Upload(string fileName, UploadCallback onComplete){bool success = fileName.EndsWith(".pdf"); // simulasi validasi sederhanaonComplete(fileName, success);}}public class Program{public static void Main(){var uploader = new Uploader();uploader.Upload("laporan.pdf", (fileName, success) =>{Console.WriteLine(success? $"{fileName} berhasil diunggah.": $"{fileName} gagal diunggah karena format tidak didukung.");});uploader.Upload("laporan.docx", (fileName, success) =>{Console.WriteLine(success? $"{fileName} berhasil diunggah.": $"{fileName} gagal diunggah karena format tidak didukung.");});}}
Contoh: Progress
C#
public delegate void ProgressHandler(int current, int total);public class BatchProcessor{public void ProcessItems(List<string> items, Action<string> processItem, ProgressHandler onProgress){for (int i = 0; i < items.Count; i++){processItem(items[i]);onProgress(i + 1, items.Count);}}}public class Program{public static void Main(){var processor = new BatchProcessor();var items = new List<string> { "item-1", "item-2", "item-3", "item-4" };processor.ProcessItems(items,item => Console.WriteLine($"Memproses {item}"),(current, total) => Console.WriteLine($"Progress: {current}/{total}"));}}
Bagian 18: Delegate dan Event
Mengapa Event Menggunakan Delegate
Event di C# dibangun di atas fondasi Delegate. Secara internal, sebuah event pada dasarnya adalah Multicast Delegate yang dibungkus dengan pembatasan akses tambahan agar bisa digunakan dengan lebih aman dalam pola publisher-subscriber. Tanpa Delegate, tidak akan ada mekanisme bagi sebuah class untuk "memberi tahu" pihak lain bahwa suatu kejadian telah terjadi, sekaligus mengizinkan pihak lain tersebut mendaftarkan sejumlah method yang ingin dijalankan sebagai respons terhadap kejadian itu.
Jika Delegate biasa digunakan langsung sebagai public field, ada risiko pihak luar bisa menggantikan seluruh invocation list menggunakan operator =, atau bahkan memanggil Delegate tersebut secara langsung dari luar class, padahal yang seharusnya boleh memanggilnya hanyalah class itu sendiri. Kata kunci event hadir untuk membatasi kemampuan tersebut, sehingga pihak luar hanya bisa menambahkan (+=) atau menghapus (-=) handler, tanpa bisa memanggil maupun menggantikan seluruh invocation list secara sepihak.
Publisher dan Subscriber
Pola Publisher-Subscriber pada Event melibatkan dua pihak: Publisher, yaitu class yang mendeklarasikan dan memicu (raise) event, dan Subscriber, yaitu class atau kode lain yang mendaftarkan method-nya (subscribe) untuk dijalankan ketika event tersebut dipicu.
C#
public class StockPriceChangedEventArgs : EventArgs{public string Symbol { get; }public decimal NewPrice { get; }public StockPriceChangedEventArgs(string symbol, decimal newPrice){Symbol = symbol;NewPrice = newPrice;}}// Publisherpublic class Stock{public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;private decimal _price;public string Symbol { get; }public Stock(string symbol, decimal initialPrice){Symbol = symbol;_price = initialPrice;}public void UpdatePrice(decimal newPrice){_price = newPrice;PriceChanged?.Invoke(this, new StockPriceChangedEventArgs(Symbol, newPrice));}}// Subscriberpublic class Program{public static void Main(){var stock = new Stock("BBCA", 9500);// Subscribe ke eventstock.PriceChanged += (sender, e) =>{Console.WriteLine($"Harga {e.Symbol} berubah menjadi {e.NewPrice}");};stock.UpdatePrice(9600); // memicu event, subscriber di atas akan dipanggil}}
Kata Kunci event
Kata kunci event dituliskan sebelum deklarasi Delegate pada sebuah field atau property, dan memberikan pembatasan akses khusus: dari luar class, pihak lain hanya bisa menggunakan operator += dan -= untuk mendaftar atau berhenti mendaftar, sedangkan pemanggilan (invoke) dan penugasan langsung (=) hanya bisa dilakukan dari dalam class tempat event tersebut dideklarasikan.
C#
public class OrderService{public event Action<string>? OrderPlaced;public void PlaceOrder(string orderId){Console.WriteLine($"Order {orderId} berhasil dibuat.");OrderPlaced?.Invoke(orderId); // hanya bisa dipanggil dari dalam class ini}}public class Program{public static void Main(){var service = new OrderService();service.OrderPlaced += orderId => Console.WriteLine($"Notifikasi: Order {orderId} telah dibuat.");// Baris di bawah ini TIDAK akan bisa dikompilasi dari luar class OrderService:// service.OrderPlaced(orderId); // tidak bisa invoke langsung dari luar// service.OrderPlaced = null; // tidak bisa menggantikan seluruh invocation listservice.PlaceOrder("ORD-001");}}
EventHandler
EventHandler dan EventHandler<TEventArgs> adalah Delegate bawaan .NET yang secara khusus dirancang untuk digunakan bersama kata kunci event, mengikuti konvensi standar .NET di mana sebuah event handler menerima dua parameter: sender (objek yang memicu event, bertipe object?) dan e (data terkait event, bertipe EventArgs atau turunannya).
C#
public delegate void EventHandler(object? sender, EventArgs e);public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
Penggunaan EventHandler tanpa data tambahan:
C#
public class Alarm{public event EventHandler? Triggered;public void Trigger(){Triggered?.Invoke(this, EventArgs.Empty);}}public class Program{public static void Main(){var alarm = new Alarm();alarm.Triggered += (sender, e) => Console.WriteLine("Alarm berbunyi!");alarm.Trigger();}}
Mengikuti konvensi ini direkomendasikan karena membuat event lebih konsisten dengan API bawaan .NET pada umumnya, sekaligus memudahkan penambahan data baru di masa depan tanpa mengubah signature event itu sendiri (cukup menambahkan property baru pada class EventArgs turunannya).
Custom Event
Selain menggunakan EventHandler<TEventArgs> dengan EventArgs sebagai basis data, Delegate custom juga tetap bisa digunakan bersama kata kunci event, terutama untuk kasus yang lebih sederhana atau ketika mengikuti konvensi EventHandler dirasa berlebihan.
C#
public class TemperatureSensor{public event Action<double>? TemperatureExceeded;private const double Threshold = 30.0;public void ReportTemperature(double temperature){Console.WriteLine($"Suhu saat ini: {temperature}°C");if (temperature > Threshold){TemperatureExceeded?.Invoke(temperature);}}}public class Program{public static void Main(){var sensor = new TemperatureSensor();sensor.TemperatureExceeded += temperature =>Console.WriteLine($"Peringatan! Suhu melebihi ambang batas: {temperature}°C");sensor.ReportTemperature(28);sensor.ReportTemperature(35);}}
Diagram Event
Secara konsep, alur kerja Event dapat digambarkan sebagai berikut:
Plain Text
Publisher (class pemilik event)│├── mendeklarasikan: event Action<T>? SomethingHappened│├── Subscriber A melakukan: publisher.SomethingHappened += HandlerA├── Subscriber B melakukan: publisher.SomethingHappened += HandlerB│▼Publisher memicu event: SomethingHappened?.Invoke(data)│├──► HandlerA(data) dijalankan└──► HandlerB(data) dijalankan
Diagram di atas menegaskan kembali bahwa Event pada dasarnya adalah Multicast Delegate (dibahas pada Bagian 6) yang dipicu oleh Publisher, dan invocation list-nya berisi seluruh handler yang telah didaftarkan oleh berbagai Subscriber. Perbedaan utamanya dengan Multicast Delegate biasa hanyalah pada pembatasan akses yang diberikan oleh kata kunci event, yang menjaga agar hanya Publisher yang bisa memicu dan mengelola siapa saja yang boleh menambah atau menghapus dirinya dari invocation list tersebut.
Perlu dicatat juga bahwa karena Subscriber menyimpan referensi ke method-nya di dalam invocation list milik Publisher, Subscriber yang lupa melakukan unsubscribe (-=) setelah tidak lagi membutuhkan event tersebut berpotensi menyebabkan memory leak, karena Publisher akan terus menahan referensi ke Subscriber tersebut selama Publisher itu sendiri masih hidup. Isu ini akan dibahas lebih detail pada Bagian 31 mengenai Anti Pattern.
Bagian 19: Covariance
Apa itu Covariance
Covariance adalah kemampuan untuk menggunakan tipe yang lebih spesifik (derived type) di tempat yang seharusnya mengharapkan tipe yang lebih umum (base type). Dalam konteks Delegate, covariance memungkinkan sebuah method dengan return type yang merupakan turunan (subclass) dari return type yang didefinisikan pada Delegate, tetap bisa ditugaskan ke Delegate tersebut.
Istilah "co" pada covariance mengacu pada arah yang searah dengan hierarki pewarisan: dari tipe turunan menuju tipe dasar, sejalan dengan prinsip bahwa sebuah objek turunan selalu bisa digunakan di mana pun objek dasar diharapkan (prinsip substitusi Liskov).
Return Type Covariance
Return Type Covariance secara spesifik merujuk pada kemampuan Delegate untuk menerima method yang return type-nya adalah turunan dari return type yang didefinisikan pada Delegate itu sendiri.
C#
public class Animal{public string Name { get; set; } = string.Empty;}public class Dog : Animal{public string Breed { get; set; } = string.Empty;}public delegate Animal AnimalFactory();public class Program{public static Dog CreateDog() => new Dog { Name = "Rex", Breed = "Golden Retriever" };public static void Main(){// Meskipun CreateDog mengembalikan Dog, bukan Animal,// ia tetap valid ditugaskan ke AnimalFactory berkat covariance.AnimalFactory factory = CreateDog;Animal animal = factory();Console.WriteLine(animal.Name); // Rex}}
Pada contoh di atas, CreateDog mengembalikan Dog, sedangkan AnimalFactory mengharapkan return type Animal. Karena Dog adalah turunan dari Animal, penugasan ini valid berkat covariance. Tanpa covariance, developer harus membuat wrapper method tambahan yang secara eksplisit mengembalikan tipe Animal.
Delegate Covariance
Delegate Covariance sudah didukung sejak C# 2.0, jauh sebelum dukungan variance pada generic interface (yang dibahas pada Bagian 21) hadir di versi yang sama. Dukungan ini berlaku otomatis tanpa memerlukan keyword tambahan seperti out atau in pada deklarasi Delegate non-generic.
C#
public class Shape { }public class Circle : Shape { }public class Rectangle : Shape { }public delegate Shape ShapeCreator();public class Program{public static Circle CreateCircle() => new Circle();public static Rectangle CreateRectangle() => new Rectangle();public static void Main(){ShapeCreator createCircle = CreateCircle;ShapeCreator createRectangle = CreateRectangle;Shape shape1 = createCircle();Shape shape2 = createRectangle();Console.WriteLine(shape1.GetType().Name); // CircleConsole.WriteLine(shape2.GetType().Name); // Rectangle}}
Inheritance
Covariance pada Delegate hanya bekerja selaras dengan hierarki pewarisan (inheritance) yang sudah ada di antara tipe-tipe yang terlibat. Tanpa hubungan pewarisan antara return type method dan return type Delegate, covariance tidak akan berlaku dan compiler akan menolak penugasan tersebut.
C#
public class Fruit { }public class Apple : Fruit { }// Vehicle tidak memiliki hubungan pewarisan dengan Fruit maupun Applepublic class Vehicle { }public delegate Fruit FruitFactory();public class Program{public static Apple CreateApple() => new Apple();public static Vehicle CreateVehicle() => new Vehicle();public static void Main(){FruitFactory factory1 = CreateApple; // valid, Apple adalah turunan Fruit// Baris di bawah ini tidak valid, karena Vehicle bukan turunan dari Fruit:// FruitFactory factory2 = CreateVehicle;}}
Diagram Pewarisan
Untuk memperjelas bagaimana covariance bekerja mengikuti arah pewarisan, berikut diagram konsepnya menggunakan contoh Animal dan Dog:
Plain Text
Animal (base type)▲│ (Dog adalah turunan dari Animal)│Dog (derived type)Delegate: AnimalFactory -> mengharapkan return type AnimalMethod: CreateDog() -> mengembalikan DogArah covariance: Dog (lebih spesifik) → Animal (lebih umum)method boleh mengembalikan tipe yang LEBIH SPESIFIKdari yang diharapkan Delegate.
Contoh Lengkap
Berikut contoh lengkap yang menggabungkan beberapa konsep sekaligus, menunjukkan bagaimana covariance memungkinkan satu Delegate Func<Employee> menampung berbagai method yang masing-masing mengembalikan tipe turunan yang berbeda-beda dari Employee.
C#
public class Employee{public string Name { get; set; } = string.Empty;}public class Manager : Employee{public int TeamSize { get; set; }}public class Intern : Employee{public string University { get; set; } = string.Empty;}public class Program{public static Manager CreateManager() => new Manager { Name = "Dewi", TeamSize = 5 };public static Intern CreateIntern() => new Intern { Name = "Anton", University = "Harvard" };public static void Main(){var factories = new List<Func<Employee>>{CreateManager, // Func<Manager> secara implisit cocok dengan Func<Employee> berkat covarianceCreateIntern // Func<Intern> juga cocok dengan cara yang sama};foreach (var factory in factories){Employee employee = factory();Console.WriteLine($"{employee.GetType().Name}: {employee.Name}");}// Output:// Manager: Dewi// Intern: Anton}}
Contoh di atas menunjukkan manfaat praktis covariance: sebuah List<Func<Employee>> bisa menampung berbagai factory method yang mengembalikan tipe-tipe turunan Employee yang berbeda-beda, tanpa perlu wrapper tambahan apa pun, karena Func<TResult> sendiri sudah dirancang bersifat covariant pada type parameter TResult-nya (ditandai dengan keyword out pada deklarasi generic-nya, yang akan dibahas lebih dalam pada Bagian 21).
Bagian 20: Contravariance
Parameter Contravariance
Jika Covariance pada Bagian 19 membahas fleksibilitas pada return type, Contravariance adalah kebalikannya: kemampuan untuk menggunakan method dengan tipe parameter yang lebih umum (base type) di tempat yang seharusnya mengharapkan tipe parameter yang lebih spesifik (derived type). Istilah "contra" mengacu pada arah yang berlawanan dengan hierarki pewarisan, berbeda dengan covariance yang searah.
Pada pandangan pertama, ini mungkin terasa berlawanan dengan intuisi: mengapa method dengan parameter yang lebih umum bisa ditugaskan ke Delegate yang mengharapkan parameter lebih spesifik? Jawabannya terletak pada prinsip bahwa method dengan parameter tipe umum sudah pasti bisa menangani semua kemungkinan tipe turunan darinya juga, sehingga aman digunakan di konteks yang lebih spesifik.
Delegate Contravariance
C#
public class Animal{public string Name { get; set; } = string.Empty;}public class Dog : Animal{public string Breed { get; set; } = string.Empty;}public delegate void DogHandler(Dog dog);public class Program{// Method ini menerima Animal (tipe lebih umum), bukan Dog (tipe lebih spesifik)public static void HandleAnimal(Animal animal){Console.WriteLine($"Menangani hewan bernama {animal.Name}");}public static void Main(){// Meskipun HandleAnimal menerima Animal, bukan Dog,// ia tetap valid ditugaskan ke DogHandler berkat contravariance.DogHandler handler = HandleAnimal;handler(new Dog { Name = "Rex", Breed = "Golden Retriever" });}}
Pada contoh di atas, DogHandler mendefinisikan parameter bertipe Dog, sedangkan HandleAnimal menerima parameter bertipe Animal (tipe dasar dari Dog). Penugasan ini valid karena HandleAnimal mampu menangani objek apa pun yang merupakan Animal, termasuk Dog, sehingga aman digunakan sebagai DogHandler.
Variance Rules
Aturan variance pada Delegate di C# dapat dirangkum sebagai berikut:
| Aspek | Arah yang Diizinkan | Contoh |
|---|---|---|
| Return Type | Covariant (boleh lebih spesifik) | Delegate mengharapkan Animal, method boleh mengembalikan Dog |
| Parameter Type | Contravariant (boleh lebih umum) | Delegate mengharapkan parameter Dog, method boleh menerima Animal |
Kedua aturan ini bekerja bersamaan dan bisa dikombinasikan dalam satu method yang sama.
Contoh Lengkap
C#
public class Publication{public string Title { get; set; } = string.Empty;}public class Book : Publication{public string Author { get; set; } = string.Empty;}public class Novel : Book{public string Genre { get; set; } = string.Empty;}public delegate void NovelProcessor(Novel novel);public class Program{// Menerima Publication (tipe paling umum dalam hierarki), bukan Novel secara spesifikpublic static void LogPublication(Publication publication){Console.WriteLine($"Memproses publikasi: {publication.Title}");}// Menerima Book (tipe menengah dalam hierarki)public static void ArchiveBook(Book book){Console.WriteLine($"Mengarsipkan buku karya {book.Author}: {book.Title}");}public static void Main(){// Ketiganya valid ditugaskan ke NovelProcessor berkat contravariance,// karena Publication dan Book sama-sama merupakan tipe dasar dari Novel.NovelProcessor processor1 = LogPublication;NovelProcessor processor2 = ArchiveBook;var novel = new Novel { Title = "Laskar Pelangi", Author = "Andrea Hirata", Genre = "Drama" };processor1(novel); // Memproses publikasi: Laskar Pelangiprocessor2(novel); // Mengarsipkan buku karya Andrea Hirata: Laskar Pelangi}}
Contoh di atas menunjukkan bahwa contravariance memungkinkan satu Delegate NovelProcessor menerima method-method yang parameternya berupa tipe apa pun di sepanjang rantai pewarisan Novel, mulai dari Publication (paling umum), Book (menengah), hingga Novel itu sendiri (paling spesifik). Kemampuan ini banyak dimanfaatkan pada Generic Delegate seperti Action<T>, yang bersifat contravariant pada type parameter-nya (ditandai dengan keyword in pada deklarasi generic-nya), sebagaimana akan dibahas lebih dalam pada Bagian 21.
Bagian 21: Variance pada Generic Delegate
out Keyword
Pada deklarasi generic interface maupun generic Delegate, keyword out digunakan untuk menandai bahwa sebuah type parameter bersifat covariant. Type parameter yang ditandai out hanya boleh digunakan pada posisi output, seperti return type, dan tidak boleh digunakan sebagai tipe parameter input. Aturan ini diperkenalkan pada C# 4.0 sebagai penyempurnaan dari Delegate Covariance yang sudah dibahas pada Bagian 19, kali ini diterapkan secara eksplisit pada level deklarasi generic.
C#
public delegate TResult Producer<out TResult>();public class Animal { }public class Dog : Animal { }public class Program{public static Dog CreateDog() => new Dog();public static void Main(){Producer<Dog> dogProducer = CreateDog;// Berkat keyword 'out', Producer<Dog> bisa digunakan sebagai Producer<Animal>Producer<Animal> animalProducer = dogProducer;Animal animal = animalProducer();Console.WriteLine(animal.GetType().Name); // Dog}}
in Keyword
Sebaliknya, keyword in menandai bahwa sebuah type parameter bersifat contravariant. Type parameter yang ditandai in hanya boleh digunakan pada posisi input, seperti parameter method, dan tidak boleh digunakan sebagai return type.
C#
public delegate void Consumer<in T>(T item);public class Animal { }public class Dog : Animal { }public class Program{public static void HandleAnimal(Animal animal) => Console.WriteLine("Menangani Animal");public static void Main(){Consumer<Animal> animalConsumer = HandleAnimal;// Berkat keyword 'in', Consumer<Animal> bisa digunakan sebagai Consumer<Dog>Consumer<Dog> dogConsumer = animalConsumer;dogConsumer(new Dog());}}
Covariant Interface
Selain pada Delegate, keyword out juga berlaku pada deklarasi generic interface. Contoh paling umum dari covariant interface bawaan .NET adalah IEnumerable<out T>.
C#
public interface IEnumerable<out T>{IEnumerator<T> GetEnumerator();}
Karena IEnumerable<T> bersifat covariant, sebuah IEnumerable<Dog> bisa digunakan di mana pun IEnumerable<Animal> diharapkan, tanpa perlu konversi eksplisit.
C#
public class Animal { }public class Dog : Animal { }public class Program{public static void PrintAnimals(IEnumerable<Animal> animals){foreach (var animal in animals){Console.WriteLine(animal.GetType().Name);}}public static void Main(){List<Dog> dogs = new() { new Dog(), new Dog() };// List<Dog> yang berupa IEnumerable<Dog> bisa langsung diteruskan// ke parameter yang mengharapkan IEnumerable<Animal>, berkat covariance.PrintAnimals(dogs);}}
Contravariant Interface
Contoh interface bawaan .NET yang bersifat contravariant adalah IComparer<in T>.
C#
public interface IComparer<in T>{int Compare(T x, T y);}
Karena IComparer<T> bersifat contravariant, sebuah IComparer<Animal> bisa digunakan di mana pun IComparer<Dog> diharapkan.
C#
public class Animal{public int Age { get; set; }}public class Dog : Animal { }public class AnimalAgeComparer : IComparer<Animal>{public int Compare(Animal? x, Animal? y) => (x?.Age ?? 0).CompareTo(y?.Age ?? 0);}public class Program{public static void Main(){var dogs = new List<Dog>{new() { Age = 3 },new() { Age = 1 },new() { Age = 5 }};// IComparer<Animal> digunakan sebagai IComparer<Dog>, berkat contravarianceIComparer<Animal> animalComparer = new AnimalAgeComparer();dogs.Sort(animalComparer);foreach (var dog in dogs){Console.WriteLine(dog.Age);}// Output terurut: 1, 3, 5}}
Generic Delegate Variance
Tipe Delegate bawaan .NET seperti Func<T, TResult> dan Action<T> sudah dirancang dengan variance yang sesuai dengan perannya masing-masing:
C#
public class Animal { }public class Dog : Animal { }public class Program{public static void Main(){// Func<Animal, Dog>: menerima Animal (tipe umum), mengembalikan Dog (tipe spesifik)Func<Animal, Dog> animalToDog = animal => new Dog();// Func<Dog, Animal> mengharapkan parameter Dog dan return type Animal.// T contravariant: parameter boleh diperluas dari Dog menjadi Animal.// TResult covariant: return type boleh dipersempit dari Animal menjadi Dog.// Kedua arah variance ini terpenuhi sekaligus, sehingga penugasan berikut valid:Func<Dog, Animal> dogToAnimal = animalToDog;Animal result = dogToAnimal(new Dog());Console.WriteLine(result.GetType().Name); // Dog}}
Pemahaman terhadap out dan in pada Generic Delegate ini menjadi penting terutama saat merancang Delegate atau interface generic custom, karena penerapan variance yang tepat memungkinkan API yang dibuat menjadi lebih fleksibel digunakan oleh pemanggilnya, tanpa mengorbankan keamanan tipe yang menjadi salah satu keunggulan utama C# sebagai bahasa yang strongly typed.
Bagian 22: Legacy Async Pattern
Mengapa Asynchronous Programming Dibutuhkan?
Operasi seperti membaca file, mengakses database, atau melakukan network request dapat membutuhkan waktu relatif lama dibandingkan operasi komputasi biasa karena operasi tersebut menunggu I/O, bukan CPU.
Jika operasi seperti ini dilakukan secara synchronous:
C#
var data = GetData();Process(data);
thread yang menjalankan kode harus berhenti dan menunggu hingga GetData() selesai sebelum melanjutkan ke baris berikutnya. Selama menunggu, thread tersebut tidak melakukan pekerjaan yang berguna, padahal dapat digunakan untuk mengerjakan pekerjaan lain.
Asynchronous programming hadir untuk mengatasi kondisi tersebut. Dengan pendekatan asynchronous, thread tidak perlu terus menunggu operasi I/O selesai. Thread dapat dibebaskan untuk mengerjakan pekerjaan lain dan kembali memproses hasil ketika operasi tersebut selesai.
Inilah alasan .NET sejak versi awal telah menyediakan berbagai mekanisme untuk menjalankan operasi secara asynchronous.
Asynchronous Programming Sebelum Task
Task bukanlah mekanisme asynchronous pertama di .NET. Sebelum Task dan Task Parallel Library (TPL) diperkenalkan, .NET telah memiliki beberapa pola asynchronous. Dua pola yang paling dikenal adalah:
-
APM (Asynchronous Programming Model), yang menggunakan pasangan method
Begin/End. -
EAP (Event-based Asynchronous Pattern), yang menggunakan event dan event handler.
Pembahasan tidak perlu mencakup seluruh API dan detail implementasi kedua pola tersebut karena keduanya kini dianggap sebagai legacy. Hal yang lebih penting adalah memahami cara kerja dasarnya dan alasan pola-pola tersebut akhirnya digantikan.
APM — Begin / End
APM menggunakan pasangan method yang biasanya memiliki awalan Begin dan End:
C#
BeginOperation(...)EndOperation(...)
Konsep dasarnya dapat digambarkan sebagai berikut:
C#
IAsyncResult result = BeginOperation(...);// Continue doing something elseEndOperation(result);
BeginOperation() memulai pekerjaan secara asynchronous dan langsung mengembalikan objek IAsyncResult tanpa menunggu pekerjaan selesai. Objek IAsyncResult digunakan untuk memantau atau merepresentasikan operasi yang sedang berlangsung. Setelah operasi selesai, EndOperation() dipanggil untuk mengambil hasil sekaligus menyelesaikan operasi tersebut.
Callback pada APM
APM juga dapat menggunakan Delegate sebagai callback sehingga EndOperation() dapat dipanggil ketika operasi selesai:
C#
BeginOperation(result =>{EndOperation(result);}, null);
Di sinilah hubungan dengan Delegate terlihat kembali. Delegate digunakan untuk menentukan method yang harus dijalankan ketika operasi asynchronous selesai. Dengan mekanisme callback, kode yang memulai operasi tidak perlu terus menunggu. Cukup disediakan method yang akan dipanggil ketika operasi tersebut selesai.
Masalah Pendekatan APM
Meskipun APM berhasil menyediakan mekanisme asynchronous, pendekatan ini memiliki beberapa kelemahan:
-
API
BeginXxx/EndXxxcukup verbose karena setiap operasi asynchronous membutuhkan pasangan method. -
Callback membuat alur program sulit dibaca, terutama ketika beberapa operasi asynchronous harus dijalankan secara berurutan.
-
Error handling menjadi kurang natural karena exception yang terjadi di dalam callback tidak dapat ditangani dengan
try/catchbiasa di sekitar pemanggilanBeginOperation(). -
Menggabungkan beberapa operasi asynchronous menjadi rumit karena setiap operasi baru dapat menghasilkan callback tambahan.
Masalah tersebut semakin terlihat ketika beberapa operasi harus dijalankan secara berurutan:
Plain Text
Begin↓Callback↓Begin↓Callback↓Begin
Semakin banyak operasi yang dirangkai, semakin dalam callback yang bersarang dan semakin sulit alur program dibaca serta dipelihara. Pola seperti ini sering disebut sebagai callback hell dan menjadi salah satu alasan .NET membutuhkan pendekatan asynchronous yang lebih baik.
EAP — Event-based Asynchronous Pattern
Sebagai pendekatan berikutnya, .NET memperkenalkan pola asynchronous berbasis event yang dikenal sebagai EAP.
Konsep sederhananya dapat digambarkan seperti berikut:
C#
OperationAsync();OperationCompleted += OnOperationCompleted;
Alih-alih memanggil EndOperation() secara langsung atau melalui callback, event handler didaftarkan pada event yang akan dipicu ketika operasi selesai. Ketika operasi selesai, event OperationCompleted akan dipicu dan method OnOperationCompleted akan dijalankan. Delegate kembali memiliki peran penting karena event pada dasarnya dibangun di atas Delegate. Event handler yang didaftarkan merupakan Delegate yang akan dipanggil ketika event dipicu.
Namun, EAP belum sepenuhnya menyelesaikan masalah APM. Beberapa masalah tetap muncul dalam bentuk berbeda:
-
Alur asynchronous tersebar di berbagai event handler yang dapat berada jauh dari kode yang memulai operasi.
-
Composition, yaitu menggabungkan beberapa operasi asynchronous secara berurutan, tetap sulit dilakukan.
-
Error handling masih kurang natural karena error biasanya disampaikan melalui properti tertentu pada event args, bukan melalui mekanisme
try/catchbiasa.
Detail seperti EventArgs khusus, IAsyncResult, dan berbagai API EAP lainnya tidak perlu dibahas secara mendalam. Hal yang penting adalah memahami bahwa EAP merupakan upaya untuk memperbaiki APM, tetapi masih belum memberikan abstraksi asynchronous yang ideal.
Mengapa Task Muncul?
Dari APM dan EAP terlihat bahwa .NET membutuhkan abstraksi asynchronous yang lebih sederhana dan mudah dikombinasikan. Kedua pola tersebut membuat asynchronous code terasa sangat berbeda dari synchronous code. Callback dan event juga membuat alur eksekusi menjadi lebih sulit diikuti ketika operasi asynchronous mulai bertambah kompleks.
Daripada terus menggunakan pola:
Plain Text
Begin → Callback → End
atau:
Plain Text
Async → Event → Handler
.NET memperkenalkan pendekatan baru yang berpusat pada satu abstraksi: Task.
Plain Text
Operation↓Task↓await↓Continue
Task merepresentasikan sebuah pekerjaan asynchronous, terlepas dari bagaimana pekerjaan tersebut dijalankan di belakang layar.
Dengan Task, operasi asynchronous dapat diperlakukan sebagai sebuah nilai yang dapat:
-
disimpan dalam variabel,
-
dikembalikan dari method,
-
digabungkan dengan operasi lain,
-
dan ditunggu hasilnya menggunakan mekanisme yang lebih natural.
Abstraksi inilah yang kemudian menjadi fondasi bagi async/await, sehingga asynchronous code dapat ditulis dengan struktur yang menyerupai synchronous code.
Hubungan dengan Delegate
Kemunculan Task tidak membuat Delegate menjadi tidak relevan. Delegate tetap banyak digunakan dalam Task Parallel Library (TPL), terutama melalui bentuk yang lebih ringkas menggunakan lambda expression.
Contohnya:
C#
Task.Run(() =>{DoWork();});
Lambda:
C#
() => DoWork()
digunakan sebagai Delegate, tepatnya Action, untuk menentukan pekerjaan yang akan dijalankan oleh Task.Run().
Jadi, meskipun sintaksnya jauh lebih ringkas dibandingkan callback pada APM atau event handler pada EAP, konsep dasarnya tetap sama: Delegate digunakan untuk menentukan method atau pekerjaan yang harus dijalankan.
Evolusinya dapat digambarkan sebagai berikut:
Plain Text
Delegate│├── Callback pada legacy async│├── Event handler│└── Action / Func pada TPL↓Task
Closing
Evolusi asynchronous programming di .NET dapat dirangkum dalam timeline berikut:
Plain Text
Legacy .NET│▼APMBeginXxx / EndXxx│▼EAPXxxAsync / XxxCompleted│▼TPLTask / Task<T>│▼async / await
Legacy Async Pattern penting dipahami bukan karena pola tersebut masih sering digunakan dalam pengembangan modern, tetapi karena pola tersebut menjelaskan alasan asynchronous programming di .NET berkembang seperti sekarang. Keterbatasan APM dan EAP, seperti verbosity, callback yang sulit dibaca, composition yang rumit, dan error handling yang kurang natural, menjadi alasan munculnya abstraksi Task. Task kemudian menjadi fondasi bagi TPL dan async/await, yang saat ini menjadi pendekatan utama untuk asynchronous programming di .NET modern.
Bagian 23: Delegate dengan Task
Setelah Bagian 22 membahas Legacy Async Pattern yang kini sudah tidak relevan di .NET modern, bagian ini membahas pendekatan yang menggantikannya: Task Parallel Library (TPL), yang menjadi fondasi utama pemrograman asynchronous di .NET Core dan .NET modern, termasuk .NET 8. Delegate tetap memegang peran penting di sini, karena hampir seluruh API pada TPL menerima Delegate (Action atau Func) sebagai parameter untuk menentukan pekerjaan apa yang harus dijalankan.
Task.Run()
Task.Run() adalah cara paling umum untuk menjalankan sebuah Delegate (biasanya Action atau Func<TResult>) secara asynchronous pada thread dari thread pool, tanpa memerlukan BeginInvoke/EndInvoke seperti pada pola lama.
C#
public class Program{public static int CalculateSum(int a, int b){System.Threading.Thread.Sleep(1000); // simulasi pekerjaan yang memakan waktureturn a + b;}public static async Task Main(){Task<int> task = Task.Run(() => CalculateSum(5, 10));Console.WriteLine("Menunggu hasil kalkulasi...");int result = await task;Console.WriteLine($"Hasil: {result}");}}
Task.Run() menerima Action untuk pekerjaan tanpa hasil, atau Func<TResult> untuk pekerjaan yang mengembalikan nilai, dan langsung mengembalikan Task atau Task<TResult> yang bisa di-await kapan pun dibutuhkan.
C#
// Task.Run dengan Action (tanpa hasil)Task task1 = Task.Run(() => Console.WriteLine("Pekerjaan tanpa hasil selesai."));// Task.Run dengan Func<TResult> (dengan hasil)Task<string> task2 = Task.Run(() => "Hasil dari pekerjaan.");
Task.Factory
Task.Factory.StartNew() adalah pendahulu dari Task.Run() yang menawarkan lebih banyak opsi konfigurasi, seperti TaskCreationOptions dan TaskScheduler kustom. Untuk kebanyakan kasus umum, Task.Run() sudah lebih dari cukup dan lebih direkomendasikan karena konfigurasi defaultnya yang lebih aman. Task.Factory.StartNew() tetap relevan untuk skenario lanjutan yang memerlukan kontrol lebih detail terhadap bagaimana Task dijalankan.
C#
public class Program{public static async Task Main(){Task<int> task = Task.Factory.StartNew(() =>{System.Threading.Thread.Sleep(500);return 42;}, TaskCreationOptions.LongRunning);int result = await task;Console.WriteLine(result);}}
Opsi TaskCreationOptions.LongRunning pada contoh di atas memberi tahu scheduler bahwa Task tersebut diperkirakan berjalan lama, sehingga sebaiknya dijalankan pada thread khusus, bukan thread pool biasa yang dioptimalkan untuk pekerjaan singkat.
Parallel
Class Parallel menyediakan method seperti Parallel.For dan Parallel.ForEach, yang menerima Delegate (Action<int> untuk Parallel.For, atau Action<T> untuk Parallel.ForEach) untuk dijalankan secara paralel terhadap sekumpulan data, memanfaatkan beberapa thread sekaligus untuk mempercepat pemrosesan data dalam jumlah besar.
C#
public class Program{public static void Main(){var numbers = Enumerable.Range(1, 10).ToList();Parallel.ForEach(numbers, number =>{int square = number * number;Console.WriteLine($"{number}^2 = {square}");});}}
Perlu diperhatikan bahwa urutan eksekusi pada Parallel.ForEach tidak terjamin berurutan seperti foreach biasa, karena setiap elemen bisa diproses oleh thread yang berbeda-beda secara bersamaan. Parallel paling cocok digunakan untuk operasi yang bersifat CPU-bound (banyak melibatkan komputasi) dan setiap elemen bisa diproses secara independen tanpa saling bergantung satu sama lain.
CancellationToken
Berbeda dengan Legacy Async Pattern yang tidak mendukung pembatalan secara native, TPL menyediakan CancellationToken sebagai mekanisme standar untuk membatalkan sebuah Task yang sedang berjalan. Delegate yang dijalankan melalui Task.Run() bisa menerima CancellationToken untuk secara berkala memeriksa apakah pembatalan telah diminta.
C#
public class Program{public static async Task Main(){using var cts = new CancellationTokenSource();CancellationToken token = cts.Token;Task task = Task.Run(() =>{for (int i = 0; i < 10; i++){token.ThrowIfCancellationRequested(); // memeriksa apakah pembatalan dimintaConsole.WriteLine($"Memproses item ke-{i}");System.Threading.Thread.Sleep(300);}}, token);await Task.Delay(1000); // menunggu sebentar sebelum membatalkancts.Cancel();try{await task;}catch (OperationCanceledException){Console.WriteLine("Task berhasil dibatalkan.");}}}
Exception
Penanganan exception pada Task jauh lebih sederhana dibandingkan Legacy Async Pattern: exception yang terjadi di dalam Delegate yang dijalankan oleh Task akan otomatis "dibungkus" ke dalam objek Task itu sendiri, dan baru akan dilempar kembali (rethrow) saat Task tersebut di-await, sehingga penanganannya cukup menggunakan try/catch konvensional.
C#
public class Program{public static async Task Main(){Task task = Task.Run(() =>{throw new InvalidOperationException("Terjadi kesalahan saat memproses data.");});try{await task;}catch (InvalidOperationException ex){Console.WriteLine($"Exception tertangkap: {ex.Message}");}}}
Apabila sebuah Task tidak pernah di-await maupun ditangani, exception yang terjadi di dalamnya bisa ter-swallow (terserap begitu saja) tanpa pernah terlihat, atau dalam kasus tertentu memicu TaskScheduler.UnobservedTaskException. Oleh karena itu praktik terbaik adalah selalu meng-await Task yang dibuat, atau setidaknya menangani exception-nya secara eksplisit melalui ContinueWith apabila await tidak memungkinkan dalam konteks tersebut.
Pemahaman terhadap TPL dan Task pada bagian ini menjadi jembatan penting menuju Bagian 24, yang akan membahas bagaimana Delegate berdampingan dengan sintaks async/await, pendekatan paling modern dan paling umum digunakan saat ini untuk menulis kode asynchronous di C#.
Bagian 24: Delegate dengan async/await
Async Lambda
Sama seperti method biasa, sebuah lambda expression juga bisa ditandai sebagai async, sehingga bisa menggunakan await di dalam tubuhnya. Lambda semacam ini disebut async lambda, dan sangat umum digunakan ketika sebuah Delegate perlu menjalankan operasi asynchronous, seperti memanggil API atau mengakses database.
C#
public class Program{public static async Task Main(){Func<Task<string>> fetchData = async () =>{await Task.Delay(500); // simulasi operasi asynchronousreturn "Data berhasil diambil";};string result = await fetchData();Console.WriteLine(result);}}
Await
Penggunaan await di dalam async lambda bekerja dengan cara yang sama seperti pada method async biasa: kode akan "berhenti" pada titik await tanpa memblokir thread, dan melanjutkan eksekusi setelah operasi asynchronous yang di-await selesai.
C#
public class Program{public static async Task<int> GetUserCountAsync(){await Task.Delay(300); // simulasi query ke databasereturn 150;}public static async Task Main(){Func<Task<int>> getUserCount = GetUserCountAsync;Console.WriteLine("Mengambil jumlah pengguna...");int count = await getUserCount();Console.WriteLine($"Jumlah pengguna: {count}");}}
Task Delegate
Istilah "Task Delegate" merujuk pada Delegate yang return type-nya berupa Task atau Task<TResult>, yang menjadi konvensi standar untuk merepresentasikan operasi asynchronous di C# modern. Delegate custom pun bisa dideklarasikan dengan return type Task, meskipun dalam praktiknya Func<Task> dan Func<Task<TResult>> bawaan .NET jauh lebih sering digunakan dibandingkan mendeklarasikan Delegate custom untuk keperluan ini.
C#
public delegate Task<bool> AsyncValidator(string input);public class Program{public static async Task<bool> ValidateAsync(string input){await Task.Delay(200); // simulasi validasi ke server eksternalreturn !string.IsNullOrWhiteSpace(input);}public static async Task Main(){AsyncValidator validator = ValidateAsync;bool isValid = await validator("haekal");Console.WriteLine(isValid);}}
Func<Task>
Func<Task> merepresentasikan sebuah method asynchronous tanpa parameter yang tidak mengembalikan nilai (setara dengan void pada method sinkron, namun tetap bisa di-await). Ini berbeda dengan Action biasa, yang meskipun bisa saja diisi dengan async lambda, tidak menyediakan cara untuk menunggu (await) selesainya operasi tersebut, sehingga rawan menyebabkan perilaku yang disebut "async void" apabila digunakan secara tidak tepat (akan dibahas pada bagian Best Practice).
C#
public class Program{public static async Task RunTaskAsync(Func<Task> operation){Console.WriteLine("Memulai operasi...");await operation();Console.WriteLine("Operasi selesai.");}public static async Task Main(){await RunTaskAsync(async () =>{await Task.Delay(500);Console.WriteLine("Operasi asynchronous sedang berjalan.");});}}
Untuk operasi asynchronous yang mengembalikan nilai, Func<Task<TResult>> digunakan sebagai gantinya.
C#
public class Program{public static async Task<TResult> RunTaskAsync<TResult>(Func<Task<TResult>> operation){Console.WriteLine("Memulai operasi...");TResult result = await operation();Console.WriteLine("Operasi selesai.");return result;}public static async Task Main(){int total = await RunTaskAsync(async () =>{await Task.Delay(300);return 100 + 200;});Console.WriteLine($"Total: {total}");}}
Action Async
Meskipun secara sintaks memungkinkan untuk membuat Action diisi dengan lambda async, praktik ini sangat tidak disarankan, karena Action (dan varian bertipe void-nya) tidak memiliki cara untuk mengembalikan Task yang bisa di-await, sehingga operasi asynchronous di dalamnya akan berjalan secara "fire and forget" tanpa kontrol penuh dari kode pemanggil.
C#
public class Program{public static void RunAction(Action operation){operation(); // tidak ada cara untuk 'await' operation() di siniConsole.WriteLine("Baris ini bisa tereksekusi SEBELUM operation() benar-benar selesai!");}public static void Main(){// Async lambda pada Action menghasilkan "async void" secara implisit — hindari pola iniRunAction(async () =>{await Task.Delay(500);Console.WriteLine("Operasi selesai (tapi kapan pastinya tidak terjamin dari sisi pemanggil).");});Console.WriteLine("Main selesai, meskipun operation() di atas mungkin belum benar-benar selesai.");}}
Masalah utama dari pola "async void" ini adalah exception yang terjadi di dalamnya tidak bisa ditangkap oleh try/catch di sekitar pemanggilan operation(), karena tidak ada Task yang bisa di-await untuk mengetahui hasil maupun exception dari operasi tersebut, exception semacam ini bisa langsung menyebabkan aplikasi crash tanpa sempat ditangani dengan baik.
Best Practice
Beberapa praktik terbaik dalam menggabungkan Delegate dengan async/await:
C#
public class NotificationService{// Overload untuk callback sinkronpublic void Send(string message, Action onSent){Console.WriteLine($"Mengirim: {message}");onSent();}// Overload untuk callback asynchronous, memberikan pilihan yang jelas dan amanpublic async Task SendAsync(string message, Func<Task> onSentAsync){Console.WriteLine($"Mengirim: {message}");await onSentAsync();}}public class Program{public static async Task Main(){var service = new NotificationService();await service.SendAsync("Pesanan Anda telah dikonfirmasi.", async () =>{await Task.Delay(200);Console.WriteLine("Log pengiriman berhasil disimpan.");});}}
Dengan menyediakan overload SendAsync yang eksplisit menerima Func<Task>, pengguna class NotificationService diarahkan untuk selalu menggunakan pola asynchronous yang benar, sehingga risiko terjadinya "async void" yang tidak disengaja bisa diminimalkan sejak dari desain API itu sendiri.
- Gunakan Func<Task> atau Func<Task<TResult>>, bukan Action, setiap kali Delegate tersebut perlu menjalankan operasi asynchronous, agar kode pemanggil tetap bisa melakukan await dan menangani exception dengan benar.
- Hindari async lambda pada parameter bertipe Action, karena akan menghasilkan pola "async void" yang sulit dikendalikan dan sulit ditangani exception-nya.
- Selalu await hasil pemanggilan Func<Task> atau Func<Task<TResult>>, jangan biarkan Task tersebut berjalan tanpa diawasi (unobserved), untuk memastikan exception di dalamnya tetap bisa ditangkap.
- Untuk API publik yang menerima Delegate sebagai parameter dan berpotensi digunakan secara asynchronous, pertimbangkan untuk secara eksplisit menyediakan overload yang menerima Func<Task>, agar penggunanya tidak tergoda menggunakan Action dengan async lambda.
Bagian 25: Delegate pada Dunia Nyata
Setelah membahas berbagai konsep teoretis pada bagian-bagian sebelumnya, bagian ini merangkum bagaimana Delegate diterapkan dalam skenario dunia nyata yang umum ditemui dalam pengembangan aplikasi, mulai dari logging hingga background job.
Logging
Delegate memungkinkan mekanisme logging yang fleksibel, di mana tujuan log (console, file, atau layanan eksternal) bisa ditentukan tanpa mengubah kode yang memanggil logger tersebut.
C#
public class Logger{private readonly Action<string> _writeLog;public Logger(Action<string> writeLog){_writeLog = writeLog;}public void Log(string message) => _writeLog($"[{DateTime.Now:HH:mm:ss}] {message}");}var consoleLogger = new Logger(message => Console.WriteLine(message));consoleLogger.Log("Aplikasi dimulai.");
Repository
Pada pola Repository, Delegate bisa digunakan untuk menyediakan query atau kondisi filter secara dinamis tanpa mengubah struktur repository itu sendiri.
C#
public class Repository<T>{private readonly List<T> _items = new();public void Add(T item) => _items.Add(item);public List<T> Find(Func<T, bool> predicate) => _items.Where(predicate).ToList();}var repository = new Repository<string>();repository.Add("Anton");repository.Add("Dewi");var results = repository.Find(name => name.StartsWith("H"));
Middleware
Delegate menjadi fondasi pola middleware, di mana setiap komponen middleware menerima "next" sebagai Delegate untuk melanjutkan ke middleware berikutnya dalam rantai pemrosesan request.
C#
public delegate Task RequestDelegate(string context);public class Program{public static RequestDelegate BuildPipeline(params Func<RequestDelegate, RequestDelegate>[] middlewares){RequestDelegate pipeline = context =>{Console.WriteLine($"Handler akhir memproses: {context}");return Task.CompletedTask;};for (int i = middlewares.Length - 1; i >= 0; i--){pipeline = middlewares[i](pipeline);}return pipeline;}public static async Task Main(){var pipeline = BuildPipeline(next => async context =>{Console.WriteLine("Middleware Logging: sebelum");await next(context);Console.WriteLine("Middleware Logging: sesudah");},next => async context =>{Console.WriteLine("Middleware Auth: memeriksa autentikasi");await next(context);});await pipeline("GET /api/orders");}}
Retry Policy
Delegate memungkinkan logika retry (percobaan ulang) dipisahkan dari logika bisnis yang sebenarnya, sehingga bisa digunakan ulang untuk operasi apa pun.
C#
public class Program{public static T Retry<T>(Func<T> operation, int maxAttempts){for (int attempt = 1; attempt <= maxAttempts; attempt++){try{return operation();}catch when (attempt < maxAttempts){Console.WriteLine($"Percobaan {attempt} gagal, mencoba lagi...");}}throw new InvalidOperationException("Seluruh percobaan gagal.");}}
Message Queue
Delegate umum digunakan sebagai handler untuk pesan yang diterima dari message queue, di mana setiap tipe pesan bisa didaftarkan dengan handler-nya masing-masing.
C#
public class MessageBus{private readonly Dictionary<string, Action<string>> _handlers = new();public void Subscribe(string topic, Action<string> handler) => _handlers[topic] = handler;public void Publish(string topic, string message){if (_handlers.TryGetValue(topic, out var handler)){handler(message);}}}var bus = new MessageBus();bus.Subscribe("order.created", message => Console.WriteLine($"Memproses order baru: {message}"));bus.Publish("order.created", "ORD-001");
Plugin
Sistem plugin sederhana bisa dibangun dengan Delegate sebagai kontrak antara aplikasi utama dan modul plugin yang bisa dimuat secara dinamis.
C#
public class PluginManager{private readonly List<Action<string>> _plugins = new();public void Register(Action<string> plugin) => _plugins.Add(plugin);public void RunAll(string input){foreach (var plugin in _plugins){plugin(input);}}}var manager = new PluginManager();manager.Register(input => Console.WriteLine($"Plugin A memproses: {input}"));manager.Register(input => Console.WriteLine($"Plugin B memproses: {input}"));manager.RunAll("data-masuk");
Game Engine
Pada game engine sederhana, Delegate sering digunakan untuk merepresentasikan perilaku (behavior) suatu entitas yang bisa berubah tergantung state permainan.
C#
public class GameCharacter{public Action Attack { get; set; } = () => Console.WriteLine("Serangan dasar.");public void PerformAttack() => Attack();}var character = new GameCharacter();character.PerformAttack(); // Serangan dasarcharacter.Attack = () => Console.WriteLine("Serangan spesial: Fireball!");character.PerformAttack(); // Serangan spesial: Fireball!
Rule Engine
Delegate memungkinkan aturan bisnis (business rule) disusun dan dievaluasi secara dinamis, sebagaimana akan dibahas lebih lengkap pada Mini Project 2 (Bagian 27).
C#
public class RuleEngine<T>{private readonly List<Func<T, bool>> _rules = new();public void AddRule(Func<T, bool> rule) => _rules.Add(rule);public bool EvaluateAll(T item) => _rules.All(rule => rule(item));}
Workflow
Delegate memungkinkan tahapan-tahapan workflow disusun secara berurutan, mirip dengan pola Pipeline yang sudah dibahas pada Bagian 17.
C#
public class WorkflowStep<T>{public string Name { get; }public Func<T, T> Execute { get; }public WorkflowStep(string name, Func<T, T> execute){Name = name;Execute = execute;}}public class Program{public static T RunWorkflow<T>(T input, params WorkflowStep<T>[] steps){T result = input;foreach (var step in steps){Console.WriteLine($"Menjalankan tahap: {step.Name}");result = step.Execute(result);}return result;}}
Validation
Sebagaimana sudah dibahas pada Bagian 7 dan Bagian 13, Delegate (khususnya Predicate<T>) sangat umum digunakan untuk menyusun rangkaian aturan validasi yang bisa dikombinasikan secara fleksibel.
C#
public class Validator<T>{private readonly List<(Func<T, bool> Rule, string ErrorMessage)> _rules = new();public Validator<T> AddRule(Func<T, bool> rule, string errorMessage){_rules.Add((rule, errorMessage));return this;}public List<string> Validate(T item){return _rules.Where(r => !r.Rule(item)).Select(r => r.ErrorMessage).ToList();}}
Background Job
Delegate digunakan secara luas pada sistem background job untuk merepresentasikan pekerjaan yang akan dijalankan secara terjadwal atau di latar belakang, seperti pada library Hangfire yang menerima Expression<Action> atau Func<Task> sebagai definisi job yang akan dijalankan.
C#
public class BackgroundJobScheduler{private readonly List<(Func<Task> Job, TimeSpan Interval)> _jobs = new();public void Schedule(Func<Task> job, TimeSpan interval) => _jobs.Add((job, interval));public async Task RunAllOnceAsync(){foreach (var (job, _) in _jobs){await job();}}}var scheduler = new BackgroundJobScheduler();scheduler.Schedule(async () =>{await Task.Delay(100);Console.WriteLine("Membersihkan cache kadaluarsa.");},TimeSpan.FromMinutes(30));
Rangkaian contoh di atas menunjukkan bahwa Delegate bukan sekadar konsep teoretis, melainkan pola yang benar-benar dipakai secara luas di hampir setiap lapisan aplikasi nyata: dari logging sederhana hingga sistem plugin dan background job yang kompleks. Tiga bagian berikutnya (Mini Project 1 hingga 3) akan menggabungkan berbagai konsep ini menjadi implementasi yang lebih lengkap dan siap dipraktikkan.
Bagian 26: Mini Project 1 — Notification System
Mini Project pertama ini menggabungkan konsep Multicast Delegate (Bagian 6), Delegate sebagai Parameter (Bagian 7), dan Action (Bagian 11) ke dalam sebuah sistem notifikasi sederhana yang mampu mengirimkan pesan melalui beberapa saluran sekaligus: Email, SMS, dan Push Notification.
Studi Kasus
Sebuah aplikasi e-commerce perlu mengirimkan notifikasi kepada pelanggan setiap kali status pesanan berubah, misalnya saat pesanan dikonfirmasi, dikirim, atau sampai tujuan. Notifikasi tersebut harus bisa dikirim melalui beberapa saluran sekaligus, dan saluran mana saja yang aktif harus bisa dikonfigurasi secara fleksibel tanpa mengubah kode inti dari sistem notifikasi itu sendiri.
Rancangan Solusi
Solusi ini memanfaatkan Multicast Delegate bertipe Action<string> sebagai wadah untuk seluruh saluran notifikasi yang aktif, di mana setiap saluran (Email, SMS, Push) didaftarkan sebagai method terpisah yang digabungkan menggunakan operator +=.
Implementasi
C#
public class NotificationChannel{public static void SendEmail(string message){Console.WriteLine($"[Email] Mengirim: \"{message}\"");}public static void SendSms(string message){Console.WriteLine($"[SMS] Mengirim: \"{message}\"");}public static void SendPushNotification(string message){Console.WriteLine($"[Push] Mengirim: \"{message}\"");}}public class NotificationService{private Action<string>? _channels;public void RegisterChannel(Action<string> channel){_channels += channel;}public void UnregisterChannel(Action<string> channel){_channels -= channel;}public void Notify(string message){if (_channels is null){Console.WriteLine("Tidak ada saluran notifikasi yang terdaftar.");return;}// Memanggil setiap handler secara manual agar kegagalan pada satu saluran// tidak menghentikan pengiriman pada saluran lain (lihat kembali Bagian 6).foreach (Action<string> channel in _channels.GetInvocationList()){try{channel(message);}catch (Exception ex){Console.WriteLine($"Gagal mengirim melalui salah satu saluran: {ex.Message}");}}}}
Penggunaan
Output yang dihasilkan:
C#
public class Program{public static void Main(){var notificationService = new NotificationService();notificationService.RegisterChannel(NotificationChannel.SendEmail);notificationService.RegisterChannel(NotificationChannel.SendSms);notificationService.RegisterChannel(NotificationChannel.SendPushNotification);Console.WriteLine("=== Notifikasi: Pesanan Dikonfirmasi ===");notificationService.Notify("Pesanan ORD-001 telah dikonfirmasi.");Console.WriteLine();Console.WriteLine("=== Menonaktifkan saluran SMS ===");notificationService.UnregisterChannel(NotificationChannel.SendSms);Console.WriteLine("=== Notifikasi: Pesanan Dikirim ===");notificationService.Notify("Pesanan ORD-001 telah dikirim.");}}
Plain Text
=== Notifikasi: Pesanan Dikonfirmasi ===[Email] Mengirim: "Pesanan ORD-001 telah dikonfirmasi."[SMS] Mengirim: "Pesanan ORD-001 telah dikonfirmasi."[Push] Mengirim: "Pesanan ORD-001 telah dikonfirmasi."=== Menonaktifkan saluran SMS ====== Notifikasi: Pesanan Dikirim ===[Email] Mengirim: "Pesanan ORD-001 telah dikirim."[Push] Mengirim: "Pesanan ORD-001 telah dikirim."
Pengembangan Lebih Lanjut
Beberapa pengembangan yang bisa ditambahkan sebagai latihan mandiri:
- Menambahkan saluran notifikasi baru, misalnya Slack atau Webhook, tanpa mengubah class NotificationService sama sekali, cukup dengan mendaftarkan method baru melalui RegisterChannel.
- Mengubah Action<string> menjadi Func<string, Task> agar setiap saluran bisa mengirim notifikasi secara asynchronous, sejalan dengan pembahasan pada Bagian 24.
- Menambahkan mekanisme prioritas, misalnya saluran Push dikirim terlebih dahulu sebelum Email, dengan menyimpan saluran dalam struktur data yang bisa diurutkan (lihat kembali Bagian 14 mengenai Comparison Delegate).
- Mencatat (logging) saluran mana saja yang gagal mengirim notifikasi ke dalam sebuah daftar terpisah, alih-alih hanya mencetaknya ke console.
Mini Project ini menunjukkan bagaimana Multicast Delegate memberikan fleksibilitas nyata dalam desain sistem: NotificationService tidak perlu tahu apa pun tentang cara kerja Email, SMS, atau Push Notification secara spesifik — ia hanya bertanggung jawab mengelola dan memanggil saluran-saluran yang telah didaftarkan kepadanya.
Bagian 27: Mini Project 2 — Rule Engine
Mini Project kedua ini menggabungkan konsep Predicate (Bagian 13), Func (Bagian 12), dan Strategy Pattern berbasis Delegate untuk membangun sebuah Rule Engine sederhana yang mampu menghitung diskon, memvalidasi voucher, dan menentukan status membership pelanggan secara dinamis.
Studi Kasus
Sebuah aplikasi e-commerce perlu menerapkan berbagai aturan diskon yang sering berubah-ubah sesuai kebijakan bisnis, misalnya diskon berdasarkan total belanja, diskon khusus member, atau diskon dari voucher tertentu. Aturan-aturan ini perlu bisa ditambah, diubah, atau dihapus tanpa mengubah struktur inti dari sistem perhitungan harga.
Rancangan Solusi
Solusi ini memanfaatkan Func<Order, decimal> sebagai representasi dari setiap aturan diskon (strategy), yang disimpan dalam sebuah daftar dan dievaluasi secara berurutan terhadap sebuah pesanan (Order), mengikuti pola Strategy yang diimplementasikan menggunakan Delegate, alih-alih menggunakan interface dan banyak class terpisah.
Implementasi
C#
public class Order{public decimal TotalAmount { get; set; }public bool IsMember { get; set; }public string? VoucherCode { get; set; }}public class DiscountRule{public string Name { get; }public Func<Order, bool> Condition { get; }public Func<Order, decimal> CalculateDiscount { get; }public DiscountRule(string name, Func<Order, bool> condition, Func<Order, decimal> calculateDiscount){Name = name;Condition = condition;CalculateDiscount = calculateDiscount;}}public class DiscountEngine{private readonly List<DiscountRule> _rules = new();public void AddRule(DiscountRule rule) => _rules.Add(rule);public decimal CalculateTotalDiscount(Order order){decimal totalDiscount = 0;foreach (var rule in _rules){if (rule.Condition(order)){decimal discount = rule.CalculateDiscount(order);Console.WriteLine($"Aturan '{rule.Name}' diterapkan: -{discount:C0}");totalDiscount += discount;}}return totalDiscount;}}
Penyusunan Aturan
Output yang dihasilkan:
C#
public class Program{public static void Main(){var engine = new DiscountEngine();// Aturan: Diskon Membershipengine.AddRule(new DiscountRule(name: "Diskon Membership",condition: order => order.IsMember,calculateDiscount: order => order.TotalAmount * 0.05m));// Aturan: Diskon Voucherengine.AddRule(new DiscountRule(name: "Diskon Voucher",condition: order => order.VoucherCode == "HEMAT50",calculateDiscount: order => 50000m));// Aturan: Diskon Pembelian Besarengine.AddRule(new DiscountRule(name: "Diskon Pembelian Besar",condition: order => order.TotalAmount >= 1000000,calculateDiscount: order => order.TotalAmount * 0.1m));var order = new Order{TotalAmount = 1200000,IsMember = true,VoucherCode = "HEMAT50"};Console.WriteLine($"Total belanja: {order.TotalAmount:C0}");Console.WriteLine();decimal totalDiscount = engine.CalculateTotalDiscount(order);decimal finalPrice = order.TotalAmount - totalDiscount;Console.WriteLine();Console.WriteLine($"Total diskon: {totalDiscount:C0}");Console.WriteLine($"Harga akhir: {finalPrice:C0}");}}
Plain Text
Total belanja: Rp1.200.000Aturan 'Diskon Membership' diterapkan: -Rp60.000Aturan 'Diskon Voucher' diterapkan: -Rp50.000Aturan 'Diskon Pembelian Besar' diterapkan: -Rp120.000Total diskon: Rp230.000Harga akhir: Rp970.000
Studi Kasus Tambahan: Status Membership
Rule Engine yang sama juga bisa diperluas untuk menentukan status membership pelanggan berdasarkan riwayat transaksi, menunjukkan bagaimana pola berbasis Delegate ini bisa digunakan ulang (reuse) untuk domain masalah yang berbeda.
C#
public class CustomerHistory{public decimal TotalSpending { get; set; }public int TransactionCount { get; set; }}public class MembershipEngine{private readonly List<(string Level, Func<CustomerHistory, bool> Condition)> _levels = new();public void AddLevel(string level, Func<CustomerHistory, bool> condition){_levels.Add((level, condition));}public string DetermineLevel(CustomerHistory history){// Diasumsikan _levels sudah disusun dari level tertinggi ke terendahforeach (var (level, condition) in _levels){if (condition(history)){return level;}}return "Reguler";}}public class Program{public static void Main(){var membershipEngine = new MembershipEngine();membershipEngine.AddLevel("Platinum", h => h.TotalSpending >= 50000000);membershipEngine.AddLevel("Gold", h => h.TotalSpending >= 20000000);membershipEngine.AddLevel("Silver", h => h.TotalSpending >= 5000000);var history = new CustomerHistory { TotalSpending = 22000000, TransactionCount = 15 };string level = membershipEngine.DetermineLevel(history);Console.WriteLine($"Status membership pelanggan: {level}"); // Gold}}
Pengembangan Lebih Lanjut
Beberapa pengembangan yang bisa ditambahkan sebagai latihan mandiri:
- Menambahkan prioritas antar aturan diskon, misalnya aturan tertentu bersifat eksklusif dan tidak boleh digabung dengan aturan lain.
- Menyimpan definisi aturan dalam bentuk data (misalnya JSON atau database) alih-alih hardcoded di dalam kode, lalu membangun Func secara dinamis menggunakan Expression Tree (topik lanjutan yang berkaitan dengan Bagian 9 mengenai Expression Lambda).
- Menambahkan batas maksimum total diskon, agar akumulasi dari berbagai aturan tidak melebihi persentase tertentu dari total belanja.
Mini Project ini menunjukkan bagaimana kombinasi Func<T, bool> sebagai kondisi dan Func<T, TResult> sebagai aksi memungkinkan aturan bisnis yang kompleks disusun secara deklaratif dan mudah diperluas, tanpa perlu membuat class terpisah untuk setiap aturan sebagaimana biasa dilakukan pada implementasi Strategy Pattern konvensional berbasis interface.
Bagian 28: Mini Project 3 — File Processor
Mini Project ketiga ini menggabungkan konsep Delegate Pipeline (Bagian 17) dan Generic Delegate (Bagian 10) untuk membangun sebuah File Processor sederhana yang memproses data melalui empat tahapan berurutan: Read File, Validate, Transform, dan Save, di mana setiap tahapan direpresentasikan sebagai Delegate yang bisa diganti secara fleksibel.
Studi Kasus
Sebuah aplikasi backend perlu memproses file data (misalnya CSV berisi data karyawan) melalui serangkaian tahapan standar: membaca file, memvalidasi setiap baris data, mengubah (transform) data ke format yang dibutuhkan, lalu menyimpan hasilnya. Setiap tahapan perlu bisa diuji secara terpisah dan diganti tanpa memengaruhi tahapan lainnya, misalnya mengganti sumber data dari file lokal menjadi API eksternal.
Rancangan Solusi
Solusi ini membangun sebuah FilePipeline<T> generic yang menerima empat Delegate: Func<string, List<T>> untuk membaca data, Predicate<T> untuk memvalidasi setiap item, Func<T, T> untuk mentransformasi item, dan Action<List<T>> untuk menyimpan hasil akhir.
Implementasi
C#
public class EmployeeRecord{public string Name { get; set; } = string.Empty;public string Department { get; set; } = string.Empty;public decimal Salary { get; set; }}public class FilePipeline<T>{private readonly Func<string, List<T>> _readFile;private readonly Predicate<T> _validate;private readonly Func<T, T> _transform;private readonly Action<List<T>> _save;public FilePipeline(Func<string, List<T>> readFile,Predicate<T> validate,Func<T, T> transform,Action<List<T>> save){_readFile = readFile;_validate = validate;_transform = transform;_save = save;}public void Run(string fileName){Console.WriteLine($"[1/4] Membaca file: {fileName}");List<T> rawData = _readFile(fileName);Console.WriteLine($" {rawData.Count} baris data berhasil dibaca.");Console.WriteLine("[2/4] Memvalidasi data...");List<T> validData = rawData.FindAll(_validate);int invalidCount = rawData.Count - validData.Count;Console.WriteLine($" {validData.Count} baris valid, {invalidCount} baris tidak valid dan diabaikan.");Console.WriteLine("[3/4] Mentransformasi data...");List<T> transformedData = validData.ConvertAll(item => _transform(item));Console.WriteLine(" Transformasi selesai.");Console.WriteLine("[4/4] Menyimpan hasil...");_save(transformedData);Console.WriteLine(" Data berhasil disimpan.");}}
Penyusunan Pipeline
Output yang dihasilkan:
C#
public class Program{public static List<EmployeeRecord> ReadFromCsv(string fileName){// Simulasi pembacaan file CSVreturn new List<EmployeeRecord>{new() { Name = "Anton", Department = "engineering", Salary = 12000000 },new() { Name = "", Department = "marketing", Salary = 9000000 }, // tidak valid: nama kosongnew() { Name = "Dewi", Department = "engineering", Salary = -500000 }, // tidak valid: gaji negatifnew() { Name = "Amar", Department = "sales", Salary = 8000000 }};}public static bool ValidateRecord(EmployeeRecord record){return !string.IsNullOrWhiteSpace(record.Name) && record.Salary > 0;}public static EmployeeRecord TransformRecord(EmployeeRecord record){return new EmployeeRecord{Name = record.Name,Department = record.Department.ToUpperInvariant(), // standarisasi menjadi huruf besarSalary = record.Salary};}public static void SaveRecords(List<EmployeeRecord> records){foreach (var record in records){Console.WriteLine($" -> Disimpan: {record.Name} | {record.Department} | {record.Salary:C0}");}}public static void Main(){var pipeline = new FilePipeline<EmployeeRecord>(readFile: ReadFromCsv,validate: ValidateRecord,transform: TransformRecord,save: SaveRecords);pipeline.Run("employees.csv");}}
Plain Text
[1/4] Membaca file: employees.csv4 baris data berhasil dibaca.[2/4] Memvalidasi data...2 baris valid, 2 baris tidak valid dan diabaikan.[3/4] Mentransformasi data...Transformasi selesai.[4/4] Menyimpan hasil...-> Disimpan: Anton| ENGINEERING | Rp12.000.000-> Disimpan: Amar| SALES | Rp8.000.000Data berhasil disimpan.
Fleksibilitas Pipeline
Karena setiap tahapan adalah Delegate yang independen, FilePipeline<T> yang sama bisa digunakan kembali dengan implementasi yang sepenuhnya berbeda tanpa mengubah class FilePipeline<T> itu sendiri, misalnya mengganti sumber data dari CSV menjadi API eksternal.
C#
public static List<EmployeeRecord> ReadFromApi(string endpoint){Console.WriteLine($" (Simulasi) Mengambil data dari endpoint: {endpoint}");return new List<EmployeeRecord>{new() { Name = "Sari", Department = "finance", Salary = 11000000 }};}var apiPipeline = new FilePipeline<EmployeeRecord>(readFile: ReadFromApi, // sumber data berbeda, tanpa mengubah FilePipeline<T>validate: ValidateRecord,transform: TransformRecord,save: SaveRecords);apiPipeline.Run("https://api.example.com/employees");
Pengembangan Lebih Lanjut
Beberapa pengembangan yang bisa ditambahkan sebagai latihan mandiri:
- Mengubah Func<string, List<T>> pada tahap _readFile menjadi Func<string, Task<List<T>>> agar pipeline mendukung pembacaan data secara asynchronous, sejalan dengan pembahasan pada Bagian 24.
- Menambahkan tahap pelaporan error, misalnya menyimpan daftar baris yang gagal validasi ke dalam file terpisah untuk ditinjau kembali, alih-alih hanya mengabaikannya.
- Mengizinkan lebih dari satu Delegate _transform yang dijalankan secara berurutan (multi-stage transform), memanfaatkan konsep Multicast Delegate atau List<Func<T, T>> yang dijalankan satu per satu.
- Menambahkan validasi dengan pesan error spesifik per aturan, menggunakan struktur seperti pada Validator<T> yang sudah dibahas pada Bagian 25.
Mini Project ini menutup rangkaian tiga Mini Project pada modul ini, menunjukkan bagaimana Delegate, mulai dari bentuk paling dasar hingga kombinasi Generic Delegate dalam sebuah pipeline hingga menjadi alat yang sangat kuat untuk membangun arsitektur aplikasi yang fleksibel, modular, dan mudah diuji.
Bagian 29: Perbandingan
Setelah membahas Delegate secara mendalam dari berbagai sudut, bagian ini membandingkan Delegate dengan beberapa konsep lain yang sering disandingkan atau bahkan disalahpahami sebagai hal yang serupa, untuk membantu menentukan kapan masing-masing pendekatan lebih tepat digunakan.
Delegate vs Interface
Baik Delegate maupun interface sama-sama memungkinkan pemisahan antara "apa yang dilakukan" dan "bagaimana melakukannya", namun keduanya cocok untuk skenario yang berbeda.
C#
// Menggunakan Delegate: cocok untuk satu perilaku sederhanapublic class PriceCalculator{private readonly Func<decimal, decimal> _taxStrategy;public PriceCalculator(Func<decimal, decimal> taxStrategy) => _taxStrategy = taxStrategy;}// Menggunakan Interface: lebih cocok ketika ada beberapa method terkaitpublic interface ITaxService{decimal CalculateTax(decimal amount);decimal CalculateExemption(decimal amount);string GetTaxCategory(decimal amount);}
Aturan praktis: gunakan Delegate ketika dependency yang dibutuhkan hanya berupa satu perilaku sederhana. Gunakan interface ketika dependency tersebut memiliki beberapa method terkait yang perlu dikelola sebagai satu kesatuan, atau ketika implementasinya perlu menyimpan state.
| Aspek | Delegate | Interface |
|---|---|---|
| Jumlah method | Cocok untuk satu perilaku spesifik | Cocok untuk sekumpulan method yang saling terkait |
| Fleksibilitas runtime | Sangat mudah diganti-ganti (lambda, method reference) | Memerlukan implementasi class terpisah |
| State (data internal) | Tidak menyimpan state sendiri | Bisa diimplementasikan oleh class yang punya state |
| Kejelasan kontrak | Kurang eksplisit untuk kontrak kompleks | Lebih eksplisit dan cocok untuk kontrak besar |
| Contoh penggunaan | Callback, event, strategi tunggal | Repository, Service, dependency yang punya banyak method |
Delegate vs Event
Sebagaimana sudah dibahas pada Bagian 18, Event sebenarnya dibangun di atas Delegate, bukan konsep yang sepenuhnya terpisah. Perbedaannya terletak pada pembatasan akses yang diberikan oleh kata kunci event.
C#
public class UnsafePublisher{public Action<string>? OnMessage; // field Delegate biasa, tidak aman}public class SafePublisher{public event Action<string>? OnMessage; // event, lebih aman}var unsafePublisher = new UnsafePublisher();unsafePublisher.OnMessage += msg => Console.WriteLine(msg);unsafePublisher.OnMessage = null; // BERBAHAYA: menghapus seluruh subscriber dari luar, ini diizinkan!unsafePublisher.OnMessage?.Invoke("test"); // juga diizinkan memanggil langsung dari luar
Aturan praktis: selalu gunakan event (bukan Delegate biasa sebagai public field) setiap kali suatu class perlu memberi tahu pihak luar tentang suatu kejadian, agar kontrol atas invocation list tetap sepenuhnya berada di tangan class yang mendeklarasikannya.
| Aspek | Delegate (field biasa) | Event |
|---|---|---|
| Invoke dari luar class | Diizinkan | Tidak diizinkan |
| Penugasan ulang (=) dari luar | Diizinkan (berisiko menghapus semua handler) | Tidak diizinkan |
| Tambah/hapus handler (+=/-=) dari luar | Diizinkan | Diizinkan |
| Kesesuaian untuk publisher-subscriber | Kurang aman | Didesain khusus untuk ini |
Delegate vs Strategy Pattern
Strategy Pattern konvensional (berbasis Gang of Four) diimplementasikan menggunakan interface dan beberapa class yang masing-masing merepresentasikan satu strategi. Delegate menawarkan cara yang lebih ringkas untuk mencapai tujuan yang sama, terutama ketika setiap strategi cukup sederhana untuk dituliskan sebagai satu ekspresi atau blok kode singkat.
C#
// Strategy Pattern konvensionalpublic interface IDiscountStrategy{decimal Calculate(decimal price);}public class MemberDiscountStrategy : IDiscountStrategy{public decimal Calculate(decimal price) => price * 0.9m;}public class NoDiscountStrategy : IDiscountStrategy{public decimal Calculate(decimal price) => price;}// Strategy Pattern berbasis Delegate (lebih ringkas, sebagaimana dibahas pada Bagian 7 dan 27)Func<decimal, decimal> memberDiscount = price => price * 0.9m;Func<decimal, decimal> noDiscount = price => price;
Strategy Pattern konvensional tetap lebih tepat digunakan ketika setiap strategi memiliki logika yang kompleks, membutuhkan dependency injection tersendiri, atau perlu diuji secara terpisah sebagai unit yang independen, sedangkan pendekatan berbasis Delegate lebih cocok untuk strategi yang ringkas dan jumlahnya bisa berubah-ubah secara dinamis, sebagaimana ditunjukkan pada Mini Project 2 (Bagian 27).
Delegate vs Command Pattern
Command Pattern membungkus sebuah permintaan (request) sebagai objek, lengkap dengan kemampuan untuk menjalankan (Execute), dan pada beberapa implementasi juga membatalkan (Undo) perintah tersebut. Delegate bisa menggantikan sebagian besar kebutuhan Command Pattern yang sederhana, namun Command Pattern konvensional tetap unggul ketika dibutuhkan riwayat perintah (command history), pembatalan, atau antrian perintah yang kompleks.
C#
// Command Pattern konvensionalpublic interface ICommand{void Execute();void Undo();}public class AddItemCommand : ICommand{private readonly List<string> _cart;private readonly string _item;public AddItemCommand(List<string> cart, string item){_cart = cart;_item = item;}public void Execute() => _cart.Add(_item);public void Undo() => _cart.Remove(_item);}// Pendekatan berbasis Delegate: cocok untuk perintah sederhana tanpa kebutuhan Undo eksplisitpublic class SimpleCommandInvoker{private readonly Stack<Action> _history = new();public void Execute(Action command){command();_history.Push(command);}}
Aturan praktis: gunakan Delegate untuk perintah sekali jalan yang sederhana dan gunakan Command Pattern konvensional ketika dibutuhkan kemampuan Undo/Redo, penyimpanan riwayat perintah yang terstruktur, atau ketika perintah tersebut perlu di-serialize (disimpan dan dijalankan kembali di lain waktu).
Delegate vs Expression Tree
Sebagaimana disinggung pada Bagian 9, Expression Lambda bisa dikonversi menjadi dua bentuk yang berbeda tergantung tipe target-nya: menjadi Delegate biasa (langsung dikompilasi menjadi IL yang siap dieksekusi), atau menjadi Expression Tree (representasi data dari struktur ekspresi itu sendiri, belum dikompilasi).
C#
using System.Linq.Expressions;// Dikompilasi langsung menjadi Delegate, siap dieksekusiFunc<int, bool> asDelegate = number => number > 0;bool result = asDelegate(5); // dieksekusi langsung// Direpresentasikan sebagai Expression Tree, BELUM dieksekusiExpression<Func<int, bool>> asExpressionTree = number => number > 0;Console.WriteLine(asExpressionTree.Body); // menampilkan struktur: (number > 0)// Expression Tree bisa dikompilasi menjadi Delegate kapan pun dibutuhkanFunc<int, bool> compiled = asExpressionTree.Compile();bool compiledResult = compiled(5);
Aturan praktis: gunakan Delegate biasa (Func, Action, dsb.) untuk logika yang memang dijalankan langsung di dalam aplikasi C#; gunakan Expression Tree ketika logika tersebut perlu dianalisis atau diterjemahkan ke bentuk lain, seperti query SQL pada Entity Framework, atau ketika membangun library yang perlu memeriksa struktur suatu ekspresi sebelum menjalankannya.
| Aspek | Delegate | Expression Tree |
|---|---|---|
| Bentuk | Kode terkompilasi (IL), siap dieksekusi | Struktur data yang merepresentasikan kode |
| Kecepatan eksekusi langsung | Cepat, tidak perlu kompilasi tambahan | Perlu di-Compile() dulu, atau diterjemahkan oleh provider lain |
| Bisa dianalisis/diterjemahkan | Tidak, sudah berupa kode mesin | Ya, misalnya diterjemahkan menjadi SQL oleh Entity Framework |
| Mendukung Statement Lambda | Ya | Tidak, hanya Expression Lambda |
Ringkasan Perbandingan
Secara umum, Delegate paling unggul untuk skenario yang membutuhkan fleksibilitas tinggi dengan kompleksitas rendah: satu perilaku sederhana yang bisa berubah-ubah secara dinamis. Ketika kebutuhan mulai melibatkan banyak method terkait (Interface), kontrol akses publisher-subscriber (Event), riwayat dan pembatalan perintah (Command Pattern), atau kebutuhan untuk menganalisis struktur logika itu sendiri (Expression Tree), konsep-konsep lain tersebut menjadi pilihan yang lebih tepat dibandingkan Delegate biasa.
Bagian 30: Best Practice
Bagian ini merangkum panduan praktis kapan sebaiknya menggunakan Delegate secara umum, dan kapan sebaiknya menggunakan Interface, Event, Func, Action, atau Predicate secara lebih spesifik, sebagai rangkuman dari seluruh pembahasan pada bagian-bagian sebelumnya.
Kapan Memakai Delegate
Delegate (baik custom maupun bawaan seperti Func/Action) paling tepat digunakan ketika:
- Perilaku yang dibutuhkan cukup sederhana dan hanya melibatkan satu method, bukan sekumpulan method yang saling terkait.
- Perilaku tersebut perlu bisa diganti secara dinamis pada saat runtime, misalnya strategi diskon yang berubah tergantung kondisi (lihat kembali Bagian 27).
- Dibutuhkan callback untuk memberi tahu pemanggil tentang progres atau hasil dari suatu proses (lihat kembali Bagian 17).
- Ingin menghindari pembuatan banyak class kecil hanya untuk merepresentasikan satu perilaku sederhana, sebagaimana dibahas pada perbandingan Delegate vs Strategy Pattern (Bagian 29).
C#
// Tepat menggunakan Delegate: satu perilaku sederhana yang bisa berubah-ubahpublic void ProcessOrder(Order order, Func<Order, decimal> discountStrategy){decimal discount = discountStrategy(order);// ...}
Kapan Memakai Interface
Interface lebih tepat digunakan ketika:
- Dependency yang dibutuhkan memiliki lebih dari satu method yang saling terkait dan perlu dikelola sebagai satu kesatuan kontrak.
- Implementasinya perlu menyimpan state (data) internal, sesuatu yang tidak bisa dilakukan oleh Delegate secara langsung.
- Dibutuhkan banyak implementasi yang bisa saling dipertukarkan secara eksplisit melalui Dependency Injection, sebagaimana umum diterapkan pada arsitektur seperti Repository atau Service layer.
C#
// Tepat menggunakan Interface: beberapa method terkait, butuh state (misalnya koneksi database)public interface IOrderRepository{Order? GetById(int id);void Save(Order order);void Delete(int id);}
Kapan Memakai Event
Event (bukan Delegate biasa sebagai public field) tepat digunakan ketika:
- Sebuah class perlu memberi tahu pihak luar tentang suatu kejadian, dan bisa jadi lebih dari satu pihak akan mendengarkan kejadian tersebut (pola publisher-subscriber).
- Dibutuhkan kontrol akses yang ketat, di mana hanya class yang mendeklarasikan event tersebut yang boleh memicu (invoke) dan mengelola invocation list-nya, sedangkan pihak luar hanya boleh mendaftar (+=) atau berhenti mendaftar (-=), sebagaimana sudah dibahas pada Bagian 18 dan 29.
C#
// Tepat menggunakan Event: notifikasi kejadian ke banyak pihak, dengan kontrol akses yang amanpublic class OrderService{public event EventHandler<OrderCreatedEventArgs>? OrderCreated;}
Kapan Memakai Func
Func tepat digunakan ketika perilaku yang dibutuhkan mengembalikan nilai, seperti kalkulasi, transformasi data, atau pengambilan keputusan yang menghasilkan sebuah nilai.
Hindari menggunakan Func<TResult> ketika sebenarnya tidak ada nilai yang perlu dikembalikan; gunakan Action untuk kasus tersebut agar maksud kode lebih jelas.
C#
// Tepat: menghitung dan mengembalikan nilaiFunc<decimal, decimal> calculateTax = amount => amount * 0.11m;Func<string, int> getLength = text => text.Length;
Kapan Memakai Action
Action tepat digunakan ketika perilaku yang dibutuhkan tidak mengembalikan nilai, seperti logging, notifikasi, atau efek samping (side effect) lainnya.
Perlu diingat kembali dari Bagian 24: hindari mengisi Action dengan async lambda, karena akan menghasilkan pola "async void" yang sulit ditangani exception-nya; gunakan Func<Task> untuk kebutuhan asynchronous.
C#
// Tepat: melakukan sesuatu tanpa mengembalikan nilaiAction<string> logMessage = message => Console.WriteLine(message);
Kapan Memakai Predicate
Predicate<T> tepat digunakan secara khusus ketika perilaku yang dibutuhkan adalah menguji suatu kondisi terhadap satu nilai dan mengembalikan bool, terutama ketika berinteraksi dengan API List<T> seperti Find, FindAll, RemoveAll, dan TrueForAll yang memang didesain menerima Predicate<T> (lihat kembali Bagian 15).
Untuk konteks LINQ, gunakan Func<T, bool> alih-alih Predicate<T>, karena operator LINQ seperti Where memang secara konsisten menggunakan Func<T, bool>, bukan Predicate<T> (lihat kembali Bagian 13 dan 16).
C#
// Tepat: digunakan bersama API List<T> yang memang mengharapkan Predicate<T>Predicate<Employee> isEligibleForBonus = employee => employee.YearsOfService >= 3;List<Employee> eligible = employees.FindAll(isEligibleForBonus);
Ringkasan Panduan Cepat
Panduan di atas bukanlah aturan yang mutlak kaku, melainkan titik awal yang baik untuk pengambilan keputusan desain. Dalam praktiknya, pemahaman mendalam terhadap trade-off masing-masing pendekatan jauh lebih penting daripada sekadar mengikuti aturan secara membabi buta, karena setiap kasus nyata bisa memiliki pertimbangan tersendiri yang perlu dievaluasi secara kontekstual.
| Kebutuhan | Rekomendasi |
|---|---|
| Satu perilaku sederhana, mudah berubah | Delegate (Func/Action bawaan) |
| Beberapa method terkait, butuh state | Interface |
| Notifikasi kejadian ke banyak pihak | Event |
| Perlu nilai kembalian | Func |
| Tidak perlu nilai kembalian | Action |
| Kondisi boolean untuk API List<T> | Predicate<T> |
| Kondisi boolean untuk LINQ | Func<T, bool> |
| Logika perlu dianalisis/diterjemahkan (mis. ke SQL) | Expression Tree |
Bagian 31: Anti Pattern
Setelah membahas berbagai praktik terbaik pada Bagian 30, bagian ini membahas kebalikannya: pola-pola bermasalah (anti pattern) yang perlu dihindari saat bekerja dengan Delegate, lengkap dengan contoh masalahnya dan cara memperbaikinya.
Delegate yang Terlalu Kompleks
Salah satu anti pattern yang paling umum adalah menuliskan lambda yang terlalu panjang dan kompleks langsung sebagai argumen dari sebuah method call, sehingga sulit dibaca dan sulit diuji secara terpisah.
C#
// Anti pattern: lambda terlalu panjang dan kompleksorders.Where(order =>{if (order.TotalAmount > 1000000 && order.IsMember){if (order.VoucherCode != null && order.VoucherCode.StartsWith("PROMO")){return order.Items.Any(item => item.Category == "Electronics") && !order.IsCancelled;}return order.CreatedAt > DateTime.Now.AddDays(-30);}return false;});
Perbaikan: ekstrak logika kompleks menjadi named method, sehingga lebih mudah dibaca, diuji, dan diberi nama yang jelas menjelaskan maksud dari kondisi tersebut.
C#
public static bool IsEligibleForPromoAnalysis(Order order){if (!order.TotalAmount > 1000000 || !order.IsMember) return false;if (order.VoucherCode?.StartsWith("PROMO") == true){return order.Items.Any(item => item.Category == "Electronics") && !order.IsCancelled;}return order.CreatedAt > DateTime.Now.AddDays(-30);}orders.Where(IsEligibleForPromoAnalysis); // jauh lebih ringkas dan jelas maksudnya
Nested Delegate
Nested Delegate merujuk pada Delegate yang isinya berisi Delegate lain secara bertumpuk (nested), yang membuat alur logika sangat sulit diikuti, terutama ketika masing-masing level memiliki kondisi dan capture variable-nya sendiri.
C#
// Anti pattern: Delegate bertumpuk yang sulit dibacaFunc<int, Func<int, Func<int, int>>> nestedCalculator = a => b => c =>{return a > 0? (b > 0 ? (c > 0 ? a + b + c : a + b) : a): 0;};int result = nestedCalculator(5)(3)(2);
Perbaikan: pecah menjadi beberapa method atau langkah yang lebih sederhana dan eksplisit, atau gunakan struktur data biasa alih-alih rangkaian Delegate bertumpuk untuk merepresentasikan beberapa parameter.
C#
public static int Calculate(int a, int b, int c){if (a <= 0) return 0;if (b <= 0) return a;if (c <= 0) return a + b;return a + b + c;}int result = Calculate(5, 3, 2); // jauh lebih mudah dibaca dan di-debug
Memory Leak karena Event
Sebagaimana sudah disinggung pada Bagian 18, Subscriber yang mendaftar ke sebuah event namun lupa melakukan unsubscribe (-=) akan terus direferensikan oleh Publisher, sehingga Garbage Collector tidak bisa membersihkan Subscriber tersebut selama Publisher masih hidup, meskipun Subscriber tersebut seharusnya sudah tidak digunakan lagi.
C#
// Anti pattern: subscribe tanpa pernah unsubscribepublic class ReportGenerator{public ReportGenerator(Stock stock){stock.PriceChanged += OnPriceChanged; // subscribe di constructor}private void OnPriceChanged(object? sender, EventArgs e){Console.WriteLine("Menghasilkan laporan baru...");}// Tidak ada method Dispose atau unsubscribe sama sekali!// Selama 'stock' masih hidup, ReportGenerator ini TIDAK AKAN PERNAH bisa di-garbage-collect,// meskipun sudah tidak ada referensi lain ke ReportGenerator dari kode luar.}
Perbaikan: implementasikan IDisposable dan lakukan unsubscribe secara eksplisit saat objek sudah tidak dibutuhkan, atau gunakan WeakEventManager/weak reference pattern untuk skenario yang lebih kompleks.
C#
public class ReportGenerator : IDisposable{private readonly Stock _stock;public ReportGenerator(Stock stock){_stock = stock;_stock.PriceChanged += OnPriceChanged;}private void OnPriceChanged(object? sender, EventArgs e){Console.WriteLine("Menghasilkan laporan baru...");}public void Dispose(){_stock.PriceChanged -= OnPriceChanged; // unsubscribe secara eksplisit}}using (var generator = new ReportGenerator(stock)){// generator digunakan di sini} // Dispose otomatis dipanggil, unsubscribe terjadi, memory leak dicegah
Anonymous Delegate yang Sulit Di-debug
Anonymous method atau lambda yang ditulis bertumpuk dan tanpa nama membuat stack trace menjadi kurang deskriptif saat terjadi exception, sehingga sulit melacak asal mula masalah, terutama pada aplikasi besar dengan banyak lambda serupa.
C#
// Anti pattern: banyak lambda anonim serupa, sulit dibedakan saat debuggingitems.ForEach(item => Process(item, x => Validate(x, y => Transform(y))));
Ketika exception terjadi di dalam salah satu lambda bertumpuk seperti ini, stack trace yang dihasilkan biasanya hanya menunjukkan nama seperti <Main>b__2_0 atau sejenisnya, yang tidak memberi informasi bermakna tentang lambda mana sebenarnya yang bermasalah.
Perbaikan: berikan nama yang jelas dengan mengekstrak lambda menjadi named method, terutama untuk logika yang lebih dari satu atau dua baris, sehingga stack trace menjadi jauh lebih informatif.
C#
public static void ProcessItem(Item item){Validate(item);Transform(item);}items.ForEach(ProcessItem); // stack trace akan menunjukkan "ProcessItem" dengan jelas apabila terjadi exception
Closure Bug
Sebagaimana sudah dibahas pada Bagian 9, closure meng-capture variabel, bukan nilai pada saat lambda dibuat. Kesalahpahaman terhadap perilaku ini adalah sumber bug yang sangat umum, terutama pada perulangan.
C#
// Anti pattern: closure bug klasik pada perulangan forvar actions = new List<Action>();for (int i = 0; i < 3; i++){actions.Add(() => Console.WriteLine(i)); // BUG: seluruh lambda meng-capture variabel 'i' yang sama}foreach (var action in actions){action(); // mencetak 3, 3, 3 -- BUKAN 0, 1, 2 seperti yang mungkin diharapkan}
Perbaikan: salin nilai variabel loop ke variabel baru di dalam scope perulangan sebelum digunakan oleh lambda, sehingga setiap lambda meng-capture variabelnya sendiri-sendiri.
C#
var actions = new List<Action>();for (int i = 0; i < 3; i++){int captured = i; // variabel baru untuk setiap iterasiactions.Add(() => Console.WriteLine(captured));}foreach (var action in actions){action(); // mencetak 0, 1, 2 sesuai harapan}
Perlu diingat kembali bahwa sejak C# 5.0, foreach (berbeda dengan for konvensional) sudah secara otomatis memberikan scope variabel baru pada setiap iterasi, sehingga bug semacam ini tidak terjadi apabila menggunakan foreach alih-alih for untuk kasus yang serupa.
Ringkasan
Kelima anti pattern di atas memiliki benang merah yang sama: Delegate yang begitu fleksibel justru bisa menjadi bumerang apabila digunakan tanpa kedisiplinan dalam hal keterbacaan kode, pengelolaan lifetime objek, dan pemahaman yang tepat terhadap bagaimana closure bekerja. Menghindari anti pattern ini pada dasarnya adalah soal menerapkan kembali prinsip-prinsip yang sudah dibahas pada Bagian 30 secara konsisten, terutama mengekstrak logika kompleks menjadi named method dan selalu memperhatikan lifecycle dari Subscriber pada Event.
Bagian 32: Performance
Bagian ini membahas aspek performa dari penggunaan Delegate, yang meskipun jarang menjadi bottleneck utama pada kebanyakan aplikasi, tetap penting dipahami terutama untuk kode yang berjalan pada hot path (bagian kode yang dieksekusi sangat sering, misalnya di dalam loop besar atau request handler dengan traffic tinggi).
Allocation
Setiap instance Delegate, termasuk lambda expression dan anonymous method, adalah reference type yang dialokasikan di heap, sebagaimana sudah disinggung pada Bagian 2 mengenai Memory Model Delegate. Ini berarti setiap kali sebuah lambda dibuat, ada biaya alokasi memori yang harus dibayar, meskipun bagi kebanyakan aplikasi biaya ini sangat kecil dan tidak signifikan.
C#
public class Program{public static void ProcessWithoutAllocation(int value){Console.WriteLine(value);}public static void Main(){for (int i = 0; i < 1000; i++){// Setiap iterasi MENGALOKASIKAN instance Delegate baru di heap,// meskipun lambda-nya terlihat identik setiap kali.Action<int> action = value => Console.WriteLine(value);action(i);}}}
Biaya alokasi ini menjadi lebih signifikan ketika terjadi berulang kali dalam jumlah sangat besar, misalnya di dalam loop yang berjalan jutaan kali, karena setiap alokasi menambah tekanan pada Garbage Collector.
GC (Garbage Collector)
Karena Delegate dialokasikan di heap, ia turut menjadi tanggung jawab Garbage Collector (GC) untuk dibersihkan ketika sudah tidak direferensikan lagi. Alokasi Delegate yang berlebihan pada hot path bisa meningkatkan frekuensi Gen 0 collection (siklus pembersihan objek berumur pendek), yang meskipun umumnya cepat, tetap memiliki biaya (overhead) yang bisa terakumulasi apabila terjadi sangat sering.
Perlu ditekankan bahwa dalam praktiknya, pada mayoritas aplikasi bisnis (seperti aplikasi web atau enterprise), biaya alokasi Delegate ini sangat jarang menjadi bottleneck nyata dibandingkan operasi lain seperti I/O, query database, atau serialisasi data. Optimisasi terkait Delegate sebaiknya hanya dilakukan setelah benar-benar terbukti menjadi masalah melalui profiling, bukan dilakukan secara prematur (premature optimization) tanpa data yang jelas.
Delegate Cache
Salah satu cara mengurangi alokasi berulang adalah dengan melakukan caching terhadap instance Delegate yang tidak berubah, alih-alih membuatnya berulang kali di dalam loop atau method yang sering dipanggil.
C#
public class Program{// Delegate di-cache sebagai field static, hanya dialokasikan sekaliprivate static readonly Action<int> CachedAction = value => Console.WriteLine(value);public static void Main(){for (int i = 0; i < 1000; i++){CachedAction(i); // tidak ada alokasi baru pada setiap iterasi}}}
Menariknya, compiler C# sebenarnya sudah melakukan optimisasi serupa secara otomatis untuk lambda yang tidak meng-capture variabel apa pun dari lingkup luar (non-capturing lambda): compiler akan meng-cache instance Delegate tersebut sebagai field static yang dibuat sekali saja, bukan pada setiap pemanggilan method.
C#
public class Program{public static void Main(){for (int i = 0; i < 1000; i++){// Lambda ini TIDAK meng-capture variabel luar,// sehingga compiler C# secara otomatis meng-cache-nya di balik layar,// hanya dialokasikan sekali meskipun ditulis di dalam loop.Action<int> action = value => Console.WriteLine(value);action(i);}}}
Optimisasi otomatis ini tidak berlaku apabila lambda tersebut meng-capture variabel dari luar, karena setiap capture yang berbeda memerlukan objek closure yang berbeda pula.
C#
public class Program{public static void Main(){for (int i = 0; i < 1000; i++){int multiplier = i; // variabel lokal yang berubah setiap iterasi// Lambda ini MENG-CAPTURE 'multiplier', sehingga TIDAK bisa di-cache secara otomatis,// objek closure baru akan dialokasikan pada setiap iterasi.Action<int> action = value => Console.WriteLine(value * multiplier);action(i);}}}
Lambda Allocation
Selain alokasi Delegate itu sendiri, lambda yang meng-capture variabel juga memerlukan alokasi tambahan untuk objek closure yang menyimpan variabel-variabel yang di-capture tersebut, sebagaimana sudah dibahas pada Bagian 9. Secara konsep, closure ini bisa dibayangkan sebagai class tersembunyi yang dihasilkan oleh compiler, berisi field-field untuk setiap variabel yang di-capture, dan Delegate yang dibuat akan menunjuk ke method pada instance closure tersebut.
C#
public static Func<int> CreateCounter(int start){int count = start;// Compiler menghasilkan class tersembunyi (kurang lebih) seperti berikut:// private sealed class <>c__DisplayClass0// {// public int count;// public int Increment() => ++count;// }return () => ++count;}
Setiap pemanggilan CreateCounter akan mengalokasikan satu instance closure baru di heap, karena setiap counter perlu menyimpan nilai count-nya sendiri secara independen.
Static Lambda
Sejak C# 9, lambda bisa ditandai dengan kata kunci static untuk secara eksplisit melarang lambda tersebut meng-capture variabel apa pun dari lingkup luar (termasuk this pada instance method). Ini berguna sebagai jaminan dari compiler bahwa tidak ada alokasi closure yang tidak disengaja, sekaligus membuat maksud kode lebih jelas bagi pembaca.
C#
public class Program{public static void Main(){// static lambda: compiler akan menolak apabila lambda ini mencoba meng-capture variabel luarFunc<int, int, int> add = static (a, b) => a + b;int threshold = 10;// Baris di bawah TIDAK akan bisa dikompilasi, karena lambda 'static'// mencoba meng-capture variabel 'threshold' dari luar:// Func<int, bool> isAboveThreshold = static value => value > threshold;}}
Penggunaan static pada lambda sangat direkomendasikan untuk lambda yang memang tidak membutuhkan akses ke variabel luar, terutama pada kode yang berjalan di hot path, karena memberikan jaminan dari compiler sekaligus dokumentasi implisit bahwa lambda tersebut aman dari closure yang tidak disengaja.
Benchmark Sederhana
Sebagai ilustrasi konseptual (bukan hasil benchmark aktual, karena hasil sesungguhnya bergantung pada environment, versi .NET, dan hardware yang digunakan), berikut gambaran umum karakteristik performa dari beberapa pendekatan yang sudah dibahas:
| Pendekatan | Alokasi per Pemanggilan | Catatan |
|---|---|---|
| Named method sebagai Delegate | Tidak ada alokasi tambahan (selain instance Delegate itu sendiri) | Paling efisien untuk method yang dipanggil berulang kali |
| Non-capturing lambda | Dialokasikan sekali (di-cache otomatis oleh compiler) | Setara dengan named method dari sisi alokasi setelah pemanggilan pertama |
| Capturing lambda (closure) | Alokasi baru pada setiap pembuatan lambda | Perlu diperhatikan pada hot path |
| Static lambda | Sama seperti non-capturing lambda | Memberikan jaminan compile-time bahwa tidak ada closure |
Untuk mengukur dampak performa secara akurat pada kasus nyata, direkomendasikan menggunakan tool seperti BenchmarkDotNet, yang mampu mengukur alokasi memori dan waktu eksekusi secara presisi, alih-alih hanya mengandalkan asumsi teoretis semata. Sebagaimana ditekankan sebelumnya, optimisasi terkait Delegate sebaiknya selalu didasarkan pada data hasil profiling yang nyata dari aplikasi yang bersangkutan, bukan dilakukan secara spekulatif tanpa bukti bahwa hal tersebut benar-benar menjadi bottleneck.
Bagian 33: Ringkasan
Bagian penutup ini merangkum keseluruhan modul dalam bentuk cheat sheet, diagram, flowchart pengambilan keputusan, dan checklist best practice, sebagai referensi cepat setelah mempelajari seluruh materi dari Bagian 1 hingga Bagian 33.
Cheat Sheet
Deklarasi dan Penugasan
C#
public delegate int Calculate(int a, int b); // Delegate customCalculate calc = (a, b) => a + b; // via lambdaCalculate calc = SomeMethod; // via named methodCalculate calc = delegate (int a, int b) { return a + b; }; // via anonymous method
Delegate Generic Bawaan
C#
Action action = () => { }; // tanpa parameter, tanpa returnAction<T> action = x => { }; // satu parameter, tanpa returnFunc<TResult> func = () => default!; // tanpa parameter, dengan returnFunc<T, TResult> func = x => default!; // satu parameter, dengan returnPredicate<T> predicate = x => true; // satu parameter, return boolComparison<T> comparison = (x, y) => 0; // dua parameter T, return int
Multicast
C#
handler += SomeMethod; // menambah method ke invocation listhandler -= SomeMethod; // menghapus method dari invocation listhandler?.Invoke(); // memanggil dengan aman meskipun null
Event
C#
public event EventHandler<TEventArgs>? SomethingHappened;SomethingHappened?.Invoke(this, new TEventArgs(...));
Async
C#
Func<Task> asyncAction = async () => { await ...; };Func<Task<TResult>> asyncFunc = async () => { await ...; return result; };// HINDARI: Action async () => { await ...; } -- menghasilkan "async void"
Diagram Ringkasan Hierarki Konsep
Markdown
Delegate (System.MulticastDelegate)│├── Custom Delegate -> deklarasi manual dengan kata kunci 'delegate'│├── Generic Delegate Bawaan│ ├── Action / Action<T...> -> tanpa return value│ ├── Func<T..., TResult> -> dengan return value│ ├── Predicate<T> -> boolean, satu parameter│ └── Comparison<T> -> perbandingan, dua parameter│├── Event -> Delegate + pembatasan akses (event keyword)│└── Async Delegate├── Func<Task> -> operasi asynchronous tanpa hasil└── Func<Task<TResult>> -> operasi asynchronous dengan hasil
Flowchart Pengambilan Keputusan
Markdown
Butuh merepresentasikan sebuah perilaku/behavior?│▼Apakah perilaku itu melibatkan LEBIH DARI SATU method terkait,atau perlu menyimpan state?│Ya ─┴─ Tidak│ │▼ ▼Interface Apakah perilaku itu berupa NOTIFIKASI kejadianke banyak pihak (publisher-subscriber)?│Ya ─┴─ Tidak│ │▼ ▼Event Apakah perlu DIANALISIS/DITERJEMAHKAN(misalnya ke SQL)?│Ya ─┴─ Tidak│ │▼ ▼Expression Tree Delegate biasa│▼Apakah mengembalikan nilai?│Ya ─┴─ Tidak│ │▼ ▼Func<...> Action<...>(atauPredicate<T>jika booleanuntuk List<T>)
Penutup
Modul ini telah membahas Delegate secara menyeluruh, mulai dari konsep paling dasar (apa itu Delegate dan bagaimana ia bekerja di balik layar), berlanjut ke penggunaan praktis (Action, Func, Predicate, LINQ, Event), topik lanjutan (variance, asynchronous programming), hingga penerapan nyata melalui tiga Mini Project dan pembahasan anti pattern serta performa. Pemahaman yang solid terhadap Delegate menjadi fondasi penting untuk memahami banyak fitur C# modern lainnya, termasuk LINQ, async/await, dan berbagai pola desain yang banyak digunakan dalam pengembangan aplikasi .NET sehari-hari.
Daftar isi
Baca juga
Artikel berikutnya

6 Agustus 2026
•
34 menit baca
Mengupas Value Type dan Reference Type di C#
Mengapa struct sering dianggap lebih cepat daripada class? Apakah benar value type selalu berada di stack dan reference type selalu berada di heap? Artikel ini membahas konsep value type, reference type, stack, heap, Garbage Collector (GC), boxing, nullable, hingga benchmark performa secara bertahap dan mudah dipahami.
Artikel lain

17 Juni 2026
•
11 menit baca
Catatan Estetika 1: Memahami Seni, Pengalaman, dan Makna Hidup (Prof. Bambang Sugiharto)
Artikel ini merupakan catatan dan refleksi dari Kuliah Estetika seri pertama bersama Prof. Bambang Sugiharto.