Menu

Func dan Action C#: Delegate Generik, Predicate dan Contohnya

Cara kerja Func dan Action di C#: Func<T, TResult> untuk fungsi yang mengembalikan nilai, Action<T> untuk fungsi yang tidak mengembalikan nilai, Predicate dan Comparison, mengirim dan mengembalikan fungsi, dictionary berisi perintah, lambda async, dan kapan sebaiknya kamu mendeklarasikan delegate sendiri.

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

Func dan Action adalah tipe delegate generik yang sudah dideklarasikan .NET untukmu, jadi kamu bisa menyimpan dan mengirim fungsi tanpa harus mendeklarasikan tipe delegate terlebih dahulu. Func mengembalikan nilai; Action tidak mengembalikan apa pun.

Output:

4
46.00
False
Hello!
Mia scored 87

Aturan yang perlu diingat: di sebuah Func, argumen tipe terakhir adalah tipe kembalian. Func<int, int, bool> menerima dua int dan mengembalikan sebuah bool. Func<int> tidak menerima apa pun dan mengembalikan sebuah int. Semua argumen tipe sebuah Action adalah parameter.

Keluarganya

TipeParameterMengembalikan
Actiontidak adavoid
Action<T>Tvoid
Action<T1, T2> ... sampai 16T1, T2, ...void
Func<TResult>tidak adaTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... sampai 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (negatif, nol, positif)
Converter<TIn, TOut>TInTOut

Predicate, Comparison dan Converter lebih tua dan muncul di method milik List<T> dan Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ memakai Func di semua tempat, itulah sebabnya list.Where(...) dan list.FindAll(...) menerima lambda dengan bentuk yang sama tapi tipe yang berbeda:

Output:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

Mengirim fungsi ke method

Parameter Func atau Action membuat pemanggil bisa menyediakan bagian algoritma yang berubah-ubah. Dua bentuk yang umum adalah "jalankan ini dan ukur waktunya" serta "coba ini sampai berhasil":

Output:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry adalah method generik, jadi Func<int, T> miliknya bisa mengembalikan apa saja; compiler menyimpulkan T sebagai string dari lambda-nya. Lambda yang dikirim ke Twice mengubah variabel lokal clicks, dan ini berhasil karena lambda menangkap variabelnya sendiri, bukan salinan nilainya.

Method group: mengirim method lewat namanya

Kamu tidak butuh lambda untuk mengisi Func atau Action. Nama method yang sudah ada dengan signature yang cocok (sebuah "method group") bisa langsung dikonversi:

Output:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) sama dengan Select(s => int.Parse(s)), hanya saja tanpa satu lapisan pemanggilan. Compiler memilih overload yang sesuai dengan tipe tujuannya: int.Parse punya beberapa overload, dan hanya Parse(string) yang cocok dengan Func<string, int>. Kalau compiler tidak bisa menentukan satu pilihan (beberapa overload sama-sama cocok, atau argumen tipe sebuah method generik tidak bisa disimpulkan dari method group), pemanggilannya tidak bisa di-compile, dan lambda yang menuliskan pemanggilannya secara eksplisit akan memperbaikinya.

Method group yang dipakai sebagai Func tetap dijalankan nanti, setiap kali delegate itu dipanggil, sama persis seperti lambda. Bedanya hanya pada cara penulisannya.

Mengembalikan fungsi

Sebuah method bisa membuat dan mengembalikan fungsi. Func yang dikembalikan membawa nilai-nilai yang dipakai untuk membuatnya:

Output:

140
180
162

Twice(staff) membuat fungsi baru yang menerapkan diskon karyawan dua kali: 200, lalu 180, lalu 162.

Dictionary berisi fungsi

Menyimpan fungsi di dalam dictionary mengubah switch yang panjang menjadi tabel pencarian. Ini bentuk yang biasa dipakai untuk penangan perintah, kalkulator dan aksi menu, dan entri baru bisa ditambahkan saat program berjalan:

Output:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

Menambah operator cukup dengan satu entri dictionary lagi, tanpa mengubah loop-nya. StringComparer.OrdinalIgnoreCase yang dikirim ke dictionary kedua membuat nama perintah tidak membedakan huruf besar dan kecil.

Fungsi async: Func<Task>, bukan Action

Pada kode async, tipe delegate menentukan apakah pemanggil bisa menunggu pekerjaannya selesai:

Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save();                       // the caller can await it and see its exceptions

Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);

Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad();                              // async void: fire and forget, exceptions escape

Lambda async yang diberikan ke Action menjadi method async void: tidak ada yang bisa melakukan await padanya, dan exception di dalamnya tidak pernah sampai ke pemanggil. Exception itu dilempar ulang di thread pool (atau di thread UI pada aplikasi desktop), dan biasanya membuat proses crash. Saat kamu menulis method yang menerima pekerjaan async, terimalah Func<Task> (atau Func<Task<T>>).

Kapan mendeklarasikan delegate sendiri

Func dan Action mencukupi sebagian besar kebutuhan. Deklarasikan tipe delegate bernama saat:

  • Signature-nya punya parameter ref, out atau in. Func tidak bisa menyatakannya, jadi fungsi bergaya TryParse butuh delegate bool TryParser<T>(string text, out T value);.
  • Namanya menjelaskan sebuah peran di API publik. delegate bool Validator(string input) di signature sebuah method memberi informasi lebih banyak daripada Func<string, bool>, dan nama parameternya muncul di IntelliSense.
  • Kamu butuh parameter params, yang juga tidak bisa dimiliki delegate generik.

Event adalah tempat lain di mana tipe delegate kustom masih muncul, walaupun EventHandler<TEventArgs> bawaan sudah mencakup sebagian besarnya. Halaman delegate membahas cara mendeklarasikan dan menggabungkan tipe delegate.

Pertanyaan yang sering diajukan

Apa perbedaan Func dan Action di C#?

Func mengembalikan nilai dan Action tidak. Di Func<int, string, bool>, argumen tipe terakhir (bool) adalah tipe kembalian dan sisanya adalah parameter. Action<int, string> menerima sebuah int dan sebuah string lalu mengembalikan void. Keduanya tersedia dalam versi dengan 0 sampai 16 parameter.

Bagaimana cara mengirim fungsi sebagai parameter di C#?

Deklarasikan parameternya sebagai Func atau Action dengan bentuk yang sesuai, lalu panggil di dalam method: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Pemanggil mengirim lambda (Apply(80m, p => p * 0.9m)) atau nama method (Apply(80m, HalfPrice)).

Apa itu Predicate<T> di C#?

Tipe delegate yang menerima T dan mengembalikan bool, bentuknya sama dengan Func<T, bool>. Tipe ini lebih tua dari Func dan dipakai oleh List<T>.Find, FindAll, RemoveAll, Exists dan Array.Find. Kedua tipe itu tidak bisa dikonversi satu sama lain, tapi sebuah lambda bisa dikonversi ke salah satunya.

Bagaimana cara memakai Func dengan kode async?

Pakai Func<Task> untuk fungsi async tanpa hasil dan Func<T, Task<TResult>> untuk fungsi async dengan hasil, supaya pemanggil bisa melakukan await. Hindari memberikan lambda async ke Action: itu membuatnya menjadi async void, yang tidak bisa di-await dan exception-nya tidak bisa ditangkap oleh pemanggil.

Apakah Func bisa punya parameter out atau ref?

Tidak. Parameter generik Func dan Action adalah parameter nilai biasa, jadi tidak ada cara untuk menulis Func<string, out int, bool>. Untuk signature seperti TryParse, deklarasikan delegate kustom: delegate bool TryParser<T>(string text, out T value);.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai