Fungsi biasa hanya bisa bekerja dengan tiga sumber data: parameter yang dikirim pemanggil, nilai yang dibuat di dalam tubuhnya, dan konstanta global. Ketika kita menulis potongan logika kecil di tengah main, data yang dibutuhkan logika itu sering kali sudah tersedia di sekitarnya — persentase diskon yang dihitung dari konfigurasi, sisa stok yang berubah sepanjang program, atau daftar nama pelanggan. Closure memiliki kemampuan yang tidak dimiliki fungsi biasa: closure bisa memakai variabel-variabel tersebut secara langsung. Kemampuan ini disebut menangkap environment, dan inilah alasan utama closure tidak sekadar “fungsi ringkas”. Pada Chapter ini kita akan melihat dua mode penangkapan — immutable dan mutable — beserta aturan mut yang menyertainya.
Apa itu Environment Closure #
Environment adalah kumpulan variabel yang berada dalam scope pada saat closure dibuat, bukan saat closure dipanggil. Ketika tubuh closure menyebut salah satu variabel tersebut, kompilator mencatatnya sebagai tangkapan (capture) dan menyertakannya sebagai bagian dari nilai closure. Mekanismenya tidak mistis: kompilator meng-generate sebuah struct anonim yang menyimpan setiap variabel yang ditangkap — berupa reference untuk penangkapan pinjaman, atau nilai utuh untuk penangkapan kepemilikan — lalu menjadikan struct itu field tersembunyi di dalam closure. Detail trait dari struct anonim ini kita bedah tuntas di Chapter Fn, FnMut, FnOnce.
Sketsa berikut memperlihatkan posisi struct anonim tersebut:
Diagram di atas menegaskan satu hal penting: closure dan variabel yang ditangkap tetap dua entitas terpisah. Variabel tetap dimiliki scope asalnya, sementara closure hanya membawa akses menuju variabel tersebut — sama seperti hubungan pemilik dan peminjam yang sudah kita kenal di Section Ownership dan Memory.
Menangkap Variabel secara Immutable #
Mode penangkapan paling ringan terjadi ketika tubuh closure hanya membaca variabel. Kompilator menangkap variabel sebagai reference immutable, sehingga closure sama sekali tidak mengganggu pemiliknya:
fn main() {
let persen_diskon: u64 = 10;
let hitung_diskon = |harga: u64| harga * persen_diskon / 100;
println!("Diskon dari Rp{} : Rp{}", 50000, hitung_diskon(50000));
println!("Diskon dari Rp{} : Rp{}", 80000, hitung_diskon(80000));
println!("Persen tetap : {}%", persen_diskon);
}Output yang Dihasilkan:
Diskon dari Rp50000 : Rp5000
Diskon dari Rp80000 : Rp8000
Persen tetap : 10%Mari kita bedah baris-baris pentingnya:
- Baris 2 (
let persen_diskon: u64 = 10;): variabelpersen_diskondibuat di scopemaindan dimiliki olehmain. Variabel ini bukan parameter closure dan bukan juga variabel lokal closure. - Baris 3 (
let hitung_diskon = |harga: u64| harga * persen_diskon / 100;): tubuh closure menyebutpersen_diskon, sehingga kompilator menangkap variabel itu ke dalam struct anonim closure — dalam bentuk reference immutable&u64. Parameterhargatetap datang dari pemanggilan, bukan dari environment. - Baris 5–6 (
hitung_diskon(50000)danhitung_diskon(80000)): closure dipanggil dua kali dan dua kali pula membacapersen_diskonlewat reference yang disimpannya. Penangkapan sekali dipakai berkali-kali — tidak ada penyalinan nilai antar pemanggilan. - Baris 7 (
println!("Persen tetap : {}%", persen_diskon);):persen_diskonmasih sah dipakaimainsecara penuh. Penangkapan immutable tidak mengambil kepemilikan dan tidak mengunci variabel dari pemiliknya.
Contoh ini memperlihatkan kenapa closure sering menggantikan “parameter tambahan”: alih-alih mengirim persen_diskon sebagai argumen ke setiap pemanggilan, closure membawa aksesnya sendiri.
Menangkap Variabel secara Mutable #
Cerita berbeda ketika tubuh closure mengubah variabel yang ditangkap. Kompilator lalu menangkap variabel sebagai reference mutable &mut, dan konsekuensinya menjalar ke dua tempat sekaligus: variabel yang ditangkap harus dideklarasikan mut, dan binding closure itu sendiri juga harus mut:
fn main() {
let mut poin_pelanggan = 0;
let mut catat_poin = |poin: u64| {
poin_pelanggan += poin;
};
catat_poin(10);
catat_poin(25);
catat_poin(40);
println!("Poin akhir pelanggan: {}", poin_pelanggan);
}Output yang Dihasilkan:
Poin akhir pelanggan: 75Mari kita bedah aturan yang bekerja di balik kode ini:
- Baris 2 (
let mut poin_pelanggan = 0;): tandamutpertama wajib karenapoin_pelangganakan diubah dari dalam closure. Tanpamutini, statementpoin_pelanggan += poindi dalam tubuh closure langsung ditolak. - Baris 3 (
let mut catat_poin = |poin: u64| {): tandamutkedua wajib pada binding closure. Alasannya: setiap kali closure mutable dipanggil, closure meminjamkan kembali&mut poin_pelangganke tubuhnya, dan meminjamkan sesuatu yang dimiliki closure berarti meminjamkan closure itu sendiri secara mutable. Binding yang tidakmuttidak boleh dipinjam mutable. - Baris 7–9 (
catat_poin(10);dst.): tiga pemanggilan menambah nilaipoin_pelanggansecara kumulatif. Nilai tersimpan di variabel asli di scopemain— closure tidak memiliki salinan sendiri, sehingga perubahan langsung terlihat dari luar. - Baris 10 (
println!("Poin akhir pelanggan: {}", poin_pelanggan);):mainmembacapoin_pelanggansetelah pemanggilan terakhir. Pinjaman mutable yang dipegang closure berakhir pada pemanggilan terakhirnya (aturan non-lexical lifetime dari Section Lifetime), sehingga pembacaan setelahnya sah.
Kode yang Gagal: Closure Tanpa mut #
Mari kita buktikan kelalaian yang disebut callout di atas. Kode berikut mendeklarasikan closure yang mengurangi stok, tetapi lupa menandai binding closure sebagai mut:
fn main() {
let mut stok_kopi = 7;
let kurangi_stok = || {
stok_kopi -= 1;
};
kurangi_stok();
println!("Stok tersisa: {}", stok_kopi);
}Kompilasi gagal dengan pesan asli berikut:
error[E0596]: cannot borrow `kurangi_stok` as mutable, as it is not declared as mutable
--> main.rs:7:5
|
4 | stok_kopi -= 1;
| --------- calling `kurangi_stok` requires mutable binding due to mutable borrow of `stok_kopi`
...
7 | kurangi_stok();
| ^^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
3 | let mut kurangi_stok = || {
| +++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0596`.Pesan E0596 patut dibaca pelan-pelan karena isinya dua lapis. Lapis pertama menjelaskan mengapa closure membutuhkan mutable binding: statement stok_kopi -= 1 menuntut mutable borrow atas stok_kopi, dan tuntutan itu menjadikan seluruh closure bermuatan mutable. Lapis kedua menunjuk di mana aturan dilanggar: pemanggilan kurangi_stok() di baris 7 meminjam closure secara mutable, padahal binding-nya tidak dideklarasikan mut. Kompilator bahkan menuliskan perbaikan lengkapnya — ubah baris deklarasi menjadi let mut kurangi_stok = || { — sehingga memperbaiki galat ini hanya soal menambah tiga karakter.
Mode Penangkapan Dipilih Kompilator #
Kita tidak pernah menuliskan “tangkap persen_diskon secara immutable” di kode. Kompilator memilih mode penangkapan setiap variabel secara otomatis dengan prinsip seringan mungkin: hanya membaca berarti reference immutable, perlu mengubah berarti reference mutable, dan hanya ketika tubuh closure memaksa kepemilikan — misalnya memindahkan nilai keluar dari environment — kompilator menangkap dengan memindahkan kepemilikan. Prinsip “seringan mungkin” inilah yang membuat closure hemat tanpa kita mengaturnya.
Satu mode penangkapan belum kita sentuh: memindahkan kepemilikan penuh ke dalam closure atas kemauan kita sendiri, bukan karena kompilator memutuskan. Mode itu dinyalakan kata kunci move dan menjadi pokok bahasan Chapter Closure dengan move.
Ringkasan #
Closure dapat memakai variabel dari scope tempat closure dibuat — kemampuan menangkap environment. Penangkapan immutable menyimpan reference biasa di struct anonim closure sehingga variabel asli tetap hidup dan bebas dipakai pemiliknya. Penangkapan mutable menuntut dua tanda mut: satu pada variabel yang ditangkap, satu pada binding closure; kelalaian pada binding memunculkan E0596 lengkap dengan saran perbaikan. Kompilator memilih mode penangkapan otomatis dengan prinsip seringan mungkin, dan mode terberat — kepemilikan penuh — dapat kita paksa dengan move.