Closure di Dart
Fungsi yang 'mengingat' variabel di sekitarnya: counter, enkapsulasi state.
Fungsi yang punya ingatan
Fungsi biasa lupa segalanya setelah selesai: variabel lokalnya hilang. Closure adalah fungsi yang menangkap dan mengingat variabel dari lingkup tempat ia dibuat, bahkan setelah lingkup itu selesai dieksekusi.
Analogi: closure itu seperti bekal makan siang. Bekal disiapkan di dapur (lingkup pembuat), dibawa pergi, dan masih bisa dimakan di kantor (dipanggil nanti). Isi bekal = variabel yang ditangkap.
Kenapa closure ada?
Untuk membuat fungsi stateful tanpa class: counter, cache, konfigurasi terkunci. Closure juga fondasi callback async: callback "mengingat" variabel lokal yang ada saat ia didaftarkan.
Bentuk dasar
Function buatCounter() {
int hitung = 0; // variabel lokal pembuat
// Closure: fungsi dalam yang menangkap 'hitung'
return () {
hitung++;
return hitung;
};
}
void main() {
var counterA = buatCounter();
var counterB = buatCounter();
print(counterA()); // 1
print(counterA()); // 2 (mengingat!)
print(counterA()); // 3
print(counterB()); // 1 (state sendiri, tidak berbagi)
}hitung seharusnya hilang saat buatCounter() selesai. Tapi closure menyimpannya hidup. Tiap pemanggilan buatCounter() menciptakan hitung baru yang independen.
Contoh kedua, closure untuk konfigurasi:
void Function(String) buatLogger(String prefix) {
return (String pesan) {
var waktu = DateTime.now().toIso8601String();
print('[$waktu][$prefix] $pesan');
};
}
void main() {
var logApp = buatLogger('APP');
var logDb = buatLogger('DB');
logApp('mulai'); // [waktu][APP] mulai
logDb('terhubung'); // [waktu][DB] terhubung
logApp('selesai'); // [waktu][APP] selesai
}Tiap logger "mengingat" prefix-nya sendiri. Tanpa closure, kamu harus oper prefix di setiap pemanggilan.
Closure menangkap variabel, bukan nilai
Poin subtle yang penting:
void main() {
var fungsi = <Function>[];
for (int i = 0; i < 3; i++) {
fungsi.add(() => print('i = $i'));
}
for (var f in fungsi) {
f();
}
// Output: i = 0, i = 1, i = 2 (Dart: tiap iterasi variabel baru)
}Di Dart, variabel loop for dianggap baru tiap iterasi, jadi tiap closure menangkap i yang berbeda. (Di JavaScript lama, ini jebakan terkenal karena semua closure berbagi satu i.)
Namun hati-hati dengan variabel yang diubah setelah closure dibuat:
void main() {
String status = 'menunggu';
var cek = () => print('Status: $status');
cek(); // Status: menunggu
status = 'selesai';
cek(); // Status: selesai (mengikuti perubahan!)
}Closure melihat nilai terkini, bukan salinan saat dibuat.
Kesalahan umum
1. Mengira closure menyalin nilai
void main() {
int x = 10;
var baca = () => x;
x = 99;
print(baca()); // 99, BUKAN 10!
}Jika butuh salinan beku, salin eksplisit ke variabel final baru di dalam pembuat.
2. Berbagi state tanpa sadar
void main() {
var counter = buatCounter();
var alias = counter; // variabel sama, state sama!
counter();
print(alias()); // 2, bukan 1
}Dua variabel menunjuk closure yang sama = satu state. Butuh state independen? Panggil pembuatnya lagi.
3. Closure dalam loop async tanpa hati-hati
Future<void> main() async {
for (int i = 0; i < 3; i++) {
Future.delayed(Duration(milliseconds: 100 * i), () {
print('tugas $i selesai');
});
}
}Di Dart ini aman (tiap iterasi i baru), tapi di bahasa lain bisa jebakan. Pahami capture semantics bahasamu.
Kesimpulan
Closure = fungsi + variabel yang ditangkapnya. Gunakan untuk counter stateful, logger terkonfigurasi, dan callback yang butuh konteks. Ingat: yang ditangkap adalah variabel (live), bukan salinan nilai.
Catatan teknis: Di balik layar, compiler Dart mengubah variabel yang ditangkap menjadi objek heap (boxing) agar hidup lebih lama dari stack frame-nya. Ini otomatis dan efisien untuk kasus umum.
Tantangan
Penghitung mundur closure
Buat buatMundur(int dari) yang mengembalikan fungsi: tiap dipanggil, nilai berkurang 1 dan mengembalikan nilai baru. Saat mencapai 0, kembalikan 0 terus.
Function buatMundur(int dari) {
// TODO
return () => 0;
}
void main() {
var mundur = buatMundur(3);
print(mundur()); // 2
print(mundur()); // 1
print(mundur()); // 0
print(mundur()); // 0
}