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:

rust
fn main() {
    let nama = "Budi";

    {
        let peran = "Admin";
        println!("{} berperan sebagai {}", nama, peran);
    }

    println!("Pengguna aktif: {}", nama);
}

Output yang Dihasilkan:

text
Budi berperan sebagai Admin
Pengguna aktif: Budi

Mari kita telaah masa hidup setiap variabel pada kode di atas:

  1. Baris 2 (let nama = "Budi";): Variabel nama lahir di scope milik main. Masa hidupnya berlangsung hingga kurung kurawal penutup main di baris 10.
  2. Baris 4 ({): Kurung kurawal ini membuka scope anak di dalam main. Scope anak berhak membaca semua variabel milik scope induknya.
  3. Baris 5 (let peran = "Admin";): Variabel peran lahir di scope anak. Masa hidupnya hanya sampai kurung kurawal penutup scope anak di baris 7.
  4. Baris 6 (println!("{} berperan sebagai {}", nama, peran);): Kedua variabel hidup berdampingan di titik ini — nama dipinjam dari scope induk, peran milik scope anak — sehingga keduanya sah untuk dicetak.
  5. Baris 7 (}): Scope anak menutup dan variabel peran mati. Memori yang dipakai peran dilepas.
  6. Baris 9 (println!("Pengguna aktif: {}", nama);): Variabel nama masih hidup karena scope main belum 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:

rust
fn main() {
    let nama = "Budi";

    {
        let peran = "Admin";
        println!("{} berperan sebagai {}", nama, peran);
    }

    println!("Peran: {}", peran);
}

Kompilasi gagal dengan galat:

text
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:

rust
fn main() {
    let judul = "Laporan Mingguan";

    for nomor in 1..=2 {
        let judul = "Laporan Harian";
        println!("{} #{}", judul, nomor);
    }

    println!("{}", judul);
}

Output yang Dihasilkan:

text
Laporan Harian #1
Laporan Harian #2
Laporan Mingguan

Mari kita telaah apa yang terjadi pada kode di atas:

  1. Baris 2 (let judul = "Laporan Mingguan";): Variabel judul pertama lahir di scope main dengan nilai teks "Laporan Mingguan".
  2. Baris 5 (let judul = "Laporan Harian";): Di dalam tubuh for (yang merupakan scope tersendiri dan dijalankan ulang tiap iterasi), let kedua menciptakan variabel baru bernama judul yang menutupi variabel lama. Keduanya sama-sama hidup, tetapi nama judul di dalam loop kini menunjuk variabel baru.
  3. Baris 6 (println!("{} #{}", judul, nomor);): Penyebutan judul di dalam loop membaca variabel yang di-shadowing, sehingga tercetak “Laporan Harian”.
  4. Baris 9 (println!("{}", judul);): Setelah loop selesai, variabel judul milik loop sudah mati tiap akhir iterasi, sehingga nama judul di scope main kembali 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:

rust
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:

text
Diskon untuk Rp100000: Rp10000
Total: Rp100000

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