Variabel di Rust tidak hidup selamanya. Setiap variabel lahir saat dideklarasikan dengan let dan mati saat kurung kurawal penutup scope-nya tercapai. Kalau kita menulis blok bersarang seperti pada Chapter sebelumnya, mungkin ada dugaan bahwa variabel di dalam blok “sudah dibersihkan entah bagaimana”. Sebenarnya tidak ada sihir: yang terjadi adalah aturan scope yang sederhana namun ditegakkan sangat ketat oleh kompiler. Pada Chapter ini kita akan menelusuri masa hidup variabel dari deklarasi hingga akhir scope, memahami pesan galat saat batas scope dilanggar, dan melihat bagaimana shadowing memungkinkan nama lama dipakai ulang secara aman.
Masa Hidup Variabel #
Aturan intinya satu kalimat: variabel dapat diakses mulai dari baris deklarasinya hingga kurung kurawal yang menutup scope tempat variabel dideklarasikan. Setiap pasangan { } membuka scope baru, dan scope dapat bersarang di dalam scope lain. Mari kita amati wujudnya:
fn main() {
let nama = "Budi";
{
let peran = "Admin";
println!("{} berperan sebagai {}", nama, peran);
}
println!("Pengguna aktif: {}", nama);
}Output yang Dihasilkan:
Budi berperan sebagai Admin
Pengguna aktif: BudiMari kita telaah masa hidup setiap variabel pada kode di atas:
- Baris 2 (
let nama = "Budi";): Variabelnamalahir di scope milikmain. Masa hidupnya berlangsung hingga kurung kurawal penutupmaindi baris 10. - Baris 4 (
{): Kurung kurawal ini membuka scope anak di dalammain. Scope anak berhak membaca semua variabel milik scope induknya. - Baris 5 (
let peran = "Admin";): Variabelperanlahir di scope anak. Masa hidupnya hanya sampai kurung kurawal penutup scope anak di baris 7. - Baris 6 (
println!("{} berperan sebagai {}", nama, peran);): Kedua variabel hidup berdampingan di titik ini —namadipinjam dari scope induk,peranmilik scope anak — sehingga keduanya sah untuk dicetak. - Baris 7 (
}): Scope anak menutup dan variabelperanmati. Memori yang dipakaiperandilepas. - Baris 9 (
println!("Pengguna aktif: {}", nama);): Variabelnamamasih hidup karena scopemainbelum selesai, sehingga baris ini sah.
Bagan mentalnya seperti boneka bersarang: scope anak hidup dan mati di dalam scope induk, dan setiap variabel hanya dikenal pada lapisan tempat variabel tersebut dilahirkan atau lapisan-lapisan anak di bawahnya.
Kompilator Menjaga Batas Scope #
Batas scope bukan konvensi sopan santun yang bisa dilanggar saat runtime — kompiler Rust memeriksa setiap penyebutan nama variabel terhadap masa hidupnya sebelum program berjalan. Coba tambahkan satu baris yang membaca peran setelah bloknya selesai:
fn main() {
let nama = "Budi";
{
let peran = "Admin";
println!("{} berperan sebagai {}", nama, peran);
}
println!("Peran: {}", peran);
}Kompilasi gagal dengan galat:
error[E0425]: cannot find value `peran` in this scope
--> src/main.rs:9:27
|
9 | println!("Peran: {}", peran);
| ^^^^^
|
help: the binding `peran` is available in a different scope in the same function
--> src/main.rs:5:13
|
5 | let peran = "Admin";
| ^^^^^Pesan galat ini layak disyukuri: kompiler tidak hanya menolak baris bermasalah, tetapi juga mengingat di baris mana variabel peran pernah hidup, lengkap dengan petunjuk bahwa variabel tersebut tersedia di scope yang berbeda. Kesalahan “variabel sudah mati tapi masih dipakai” yang di bahasa lain baru terlihat saat runtime, di Rust mati sejak kompilasi.
Shadowing: Nama Baru di Scope Baru #
Bagaimana jika kita sengaja ingin memakai nama yang sama? Rust mengizinkannya lewat shadowing: let baru dengan nama yang sama membuat variabel baru yang menutupi variabel lama selama scope variabel baru berlangsung. Contoh paling jelas terjadi di dalam loop:
fn main() {
let judul = "Laporan Mingguan";
for nomor in 1..=2 {
let judul = "Laporan Harian";
println!("{} #{}", judul, nomor);
}
println!("{}", judul);
}Output yang Dihasilkan:
Laporan Harian #1
Laporan Harian #2
Laporan MingguanMari kita telaah apa yang terjadi pada kode di atas:
- Baris 2 (
let judul = "Laporan Mingguan";): Variabeljudulpertama lahir di scopemaindengan nilai teks"Laporan Mingguan". - Baris 5 (
let judul = "Laporan Harian";): Di dalam tubuhfor(yang merupakan scope tersendiri dan dijalankan ulang tiap iterasi),letkedua menciptakan variabel baru bernamajudulyang menutupi variabel lama. Keduanya sama-sama hidup, tetapi namajuduldi dalam loop kini menunjuk variabel baru. - Baris 6 (
println!("{} #{}", judul, nomor);): Penyebutanjuduldi dalam loop membaca variabel yang di-shadowing, sehingga tercetak “Laporan Harian”. - Baris 9 (
println!("{}", judul);): Setelah loop selesai, variabeljudulmilik loop sudah mati tiap akhir iterasi, sehingga namajuduldi scopemainkembali terlihat dan tercetak “Laporan Mingguan”.
Perbedaan shadowing dengan menimpa nilai (judul = "Laporan Harian";) fundamental: shadowing menciptakan variabel baru di scope baru (bahkan boleh dengan tipe berbeda), sedangkan penimpaan mengubah isi variabel lama dan hanya sah untuk tipe yang sama.
Scope pada Parameter dan Variabel Fungsi #
Fungsi adalah benteng scope yang paling kokoh. Parameter dan variabel lokal sebuah fungsi sama sekali tidak terlihat dari luar fungsi tersebut, sekalipun pemanggilnya memiliki variabel bernama sama:
fn tampilkan_diskon(total: i64) {
let diskon = total / 10;
println!("Diskon untuk Rp{}: Rp{}", total, diskon);
}
fn main() {
let total = 100000;
tampilkan_diskon(total);
println!("Total: Rp{}", total);
}Output yang Dihasilkan:
Diskon untuk Rp100000: Rp10000
Total: Rp100000Tiga fakta penting tampak pada kode di atas. Pertama, parameter total pada baris 1 dan variabel total pada baris 7 adalah dua variabel berbeda yang kebetulan bernama sama; fungsi tampilkan_diskon memegang salinannya sendiri, dan keduanya tidak saling mengganggu. Kedua, variabel diskon di baris 2 lahir dan mati di dalam fungsi — pemanggil tidak bisa membacanya meskipun diskon baru saja dipakai mencetak angka. Ketiga, inilah alasan kuat mengapa fungsi yang perlu membagikan hasil perhitungan harus mengembalikan nilai lewat return value, bukan menyimpannya di variabel lokal.
Ringkasan #
Variabel hidup dari baris let hingga kurung kurawal penutup scope-nya, dan kompiler menolak setiap akses di luar masa hidup tersebut dengan galat cannot find value ... in this scope. Blok bersarang membuka scope anak yang dapat membaca variabel induk, sementara variabel anak tidak pernah bocor keluar. Shadowing memungkinkan nama sama dipakai di scope berbeda tanpa konflik, dan fungsi memagari parameter serta variabel lokalnya dari dunia luar. Pemahaman scope ini melengkapi perangkat kita untuk bab pamungkas section ini: fungsi dengan banyak jalur return.