Lambda adalah fungsi anonim yang ditulis dengan tanda => dan dapat dikonversi menjadi delegate maupun menjadi expression tree, yaitu struktur data yang menyimpan kode sebagai data. Topik ini memperdalam dua sifat lambda yang sering menentukan kebenaran program, yaitu bentuk konversinya dan semantik penangkapan variabel luar. Bagian akhir mengenalkan expression tree beserta kegunaannya pada penyedia LINQ (provider). Setelah topik ini, perilaku lambda yang melibatkan variabel luar dapat diprediksi, dan perbedaan lambda yang dijalankan dengan lambda yang dianalisis menjadi jelas.
Bentuk lambda dan konversinya#
Lambda memiliki dua bentuk, yaitu lambda ekspresi yang badannya satu ekspresi tanpa kurung kurawal, dan lambda pernyataan yang badannya berupa blok dengan kurung kurawal serta return yang eksplisit. Keduanya dapat dikonversi ke tipe delegate yang tanda tangannya cocok, misalnya Action untuk metode tanpa nilai kembali dan Func untuk metode dengan nilai kembali. Program utuh berikut memperlihatkan keduanya, sekaligus menyiapkan contoh penangkapan variabel. Salin programnya ke berkas Program.cs, lalu jalankan dengan dotnet run --file Program.cs.
// Program pendalaman lambda: bentuk, konversi, dan penangkapan variabel.
Func<int, int, int> jumlahkan = (a, b) => a + b;
Console.WriteLine($"Lambda sebagai delegate: {jumlahkan(7, 3)}");
// Variabel batas ditangkap lambda, bukan disalin nilainya.
int batas = 15;
Func<int, bool> melebihiBatas = nilai => nilai > batas;
Console.WriteLine($"Dengan batas 15, nilai 12 melebihi batas: {melebihiBatas(12)}");
// Mengubah batas mengubah perilaku lambda yang sama.
batas = 10;
Console.WriteLine($"Dengan batas 10, nilai 12 melebihi batas: {melebihiBatas(12)}");
// Lambda berbadan pernyataan memakai kurung kurawal dan return.
Func<int, string> klasifikasikan = nilai =>
{
if (nilai >= 80)
{
return "Baik";
}
return "Perlu latihan";
};
Console.WriteLine(klasifikasikan(85));Lambda sebagai delegate: 10
Dengan batas 15, nilai 12 melebihi batas: False
Dengan batas 10, nilai 12 melebihi batas: True
BaikBaris pertama program mendemonstrasikan lambda ekspresi (a, b) => a + b yang dikonversi ke Func<int, int, int>, sedangkan lambda klasifikasikan memperlihatkan lambda pernyataan yang badannya memuat percabangan. Apabila badan lambda memerlukan lebih dari satu pernyataan, bentuk pernyataan adalah satu-satunya pilihan, dan kata kunci return wajib dituliskan karena tidak ada ekspresi tunggal yang nilainya otomatis dikembalikan.
Tipe parameter pada lambda boleh dituliskan secara eksplisit, misalnya (int a, int b) => a + b, atau dibiarkan implisit sebagaimana program di atas. Kompilator menurunkan tipenya dari tipe delegate tujuan, sehingga penulisan implisit tidak membuat lambda kehilangan pemeriksaan tipe sama sekali. Apabila tipe delegate tujuan berubah, tipe seluruh parameter mengikuti tanpa penulisan ulang pada lambda.
Semantik penangkapan variabel#
Lambda yang merujuk variabel di luar badannya tidak menyalin nilai variabel itu, melainkan menangkap variabelnya (captured variable), sehingga lambda dan lingkup asalnya membagi satu variabel yang sama. Perubahan pada variabel dari sisi mana pun terlihat oleh kedua sisi, dan itu terlihat pada dua baris tengah keluaran program di atas. Argumen 12 tidak berubah, tetapi hasilnya berbalik dari False menjadi True setelah variabel batas diubah dari 15 menjadi 10.
Penangkapan juga memperpanjang masa hidup variabel yang ditangkap. Variabel lokal biasanya mati saat metodenya selesai, tetapi variabel yang ditangkap tetap hidup selama ada delegate yang membutuhkannya, dan pemulung memori (garbage collector) tidak mengambilnya sebelum delegate terakhir hilang. Karena itu lambda dapat membawa keadaan (state) melewati batas metode, dan kombinasi lambda dengan variabel yang ditangkap itulah yang disebut closure, yaitu fungsi yang membawa lingkungannya.
Mengenal expression tree#
Konversi lambda tidak selalu menghasilkan kode yang langsung dijalankan. Apabila lambda dikonversi ke tipe Expression<TDelegate>, kompilator menghasilkan expression tree, yaitu pohon objek yang menggambarkan struktur kode, mulai dari parameternya hingga operasinya. Lambda pada bentuk ini bukan mesin yang berjalan, melainkan data yang dapat diperiksa, dianalisis, dan diterjemahkan menjadi bentuk lain, misalnya perintah SQL oleh penyedia LINQ.
// Lambda yang sama, dua target konversi yang berbeda.
using System.Linq.Expressions;
Func<int, int> kuadratJalan = x => x * x;
Expression<Func<int, int>> kuadratData = x => x * x;
Console.WriteLine($"Func dijalankan: {kuadratJalan(6)}");
Console.WriteLine($"Expression hanya data: {kuadratData.Body}");
Console.WriteLine($"Jenis simpul badan: {kuadratData.Body.NodeType}");Func dijalankan: 36
Expression hanya data: (x * x)
Jenis simpul badan: MultiplyKedua lambda pada contoh ditulis sama persis, tetapi target konversinya memisahkan dunia keduanya. Func<int, int> dikompilasi menjadi instruksi yang dapat langsung dipanggil dengan kuadratJalan(6), sedangkan Expression<Func<int, int>> hanya menyimpan deskripsi perkalian yang dapat dibaca dari properti Body dan Parameters. Metode Compile() dapat mengembalikan pohon tersebut menjadi delegate yang siap dijalankan, sehingga jalur dari data kembali ke kode tetap terbuka.
Perbedaan kedua target tersebut paling terasa pada kueri LINQ. Kumpulan di memori seperti List<T> menerima delegate, sehingga lambda dijalankan seketika di dalam proses. Kumpulan IQueryable<T> menerima Expression, sehingga pohon yang diterimanya dianalisis dan diterjemahkan menjadi bentuk lain, misalnya pernyataan SQL pada akses basis data. Lambda yang sama dapat mengalami nasib yang berbeda semata karena tipe kumpulan yang menerimanya berbeda.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika lambda digunakan lebih dalam.
- Tertukar antara
FuncdanExpressionsaat lambda dikirim sebagai argumen.Funcdievaluasi langsung di memori, sedangkanExpressionditerjemahkan penyedia LINQ ke bentuk lain, sehingga lambda yang sama dapat menghasilkan perilaku yang berbeda sekadar karena tipe parameternya berbeda. - Lambda pernyataan ditulis tanpa
returnatau tanpa kurung kurawal. Kompilator menolaknya karena tipe kembaliannya tidak terpenuhi, sehingga badan yang lebih dari satu pernyataan selalu ditulis sebagai blok utuh. - Variabel yang ditangkap dianggap salinan nilainya. Kode yang mengubah variabel setelah lambda dibuat akan mengubah perilaku lambda, dan jebakan khusus pada variabel perulangan dibahas pada topik Closure.
Ringkasan#
- Lambda memiliki bentuk ekspresi dan bentuk pernyataan, dan keduanya dikonversi ke tipe delegate yang tanda tangannya cocok.
- Lambda menangkap variabel luar secara utuh, bukan menyalin nilainya, sehingga perubahan dari salah satu sisi terlihat oleh keduanya dan masa hidup variabel ikut memanjang.
- Konversi ke
Expression<TDelegate>menghasilkan expression tree yang menyimpan kode sebagai data dan dapat diterjemahkan oleh penyedia LINQ.
Referensi#
- Lambda expressions and anonymous functions — Microsoft Learn — Referensi lambda, aturan konversi, dan penangkapan variabel luar.
- Expression Trees — Microsoft Learn — Panduan expression tree beserta analisis dan penerjemahannya.