Pendalaman Lambda

Semantik penangkapan lambda dan pengenalan expression tree.

Daftar isi

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.

csharp 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));
text Keluaran
Lambda sebagai delegate: 10
Dengan batas 15, nilai 12 melebihi batas: False
Dengan batas 10, nilai 12 melebihi batas: True
Baik

Baris 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.

csharp Contoh: lambda yang sama pada dua target konversi
// 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}");
text Keluaran
Func dijalankan: 36
Expression hanya data: (x * x)
Jenis simpul badan: Multiply

Kedua 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 Func dan Expression saat lambda dikirim sebagai argumen. Func dievaluasi langsung di memori, sedangkan Expression diterjemahkan penyedia LINQ ke bentuk lain, sehingga lambda yang sama dapat menghasilkan perilaku yang berbeda sekadar karena tipe parameternya berbeda.
  • Lambda pernyataan ditulis tanpa return atau 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#