Perhitungan nyata jarang selesai dalam satu langkah. Menghitung harga akhir menuntut subtotal dulu, lalu diskon, baru hasil akhirnya. Kalau setiap langkah kita ikat ke variabel di fungsi main, fungsi tersebut perlahan dipenuhi variabel perantara yang sebenarnya sudah tidak relevan setelah dipakai sekali. Pembaca kode terpaksa mengingat seluruh riwayat perhitungan, padahal yang dibutuhkan hanya hasil akhirnya. Pada Chapter ini kita akan memanfaatkan fakta bahwa blok { } di Rust adalah expression yang bernilai, sehingga perhitungan bertahap dapat dikunci di dalam blok bersarang dan hanya hasil akhirnya yang keluar ke variabel.

Masalah Variabel Perantara yang Mengotori Scope #

Mari kita lihat dulu gejalanya. Program berikut menghitung total belanja setelah diskon dengan langkah per langkah, semua ditulis rata di main:

rust
fn main() {
    let harga_satuan = 25000;
    let jumlah = 4;
    let subtotal = harga_satuan * jumlah;
    let diskon = subtotal / 10;
    let total = subtotal - diskon;

    println!("Total: Rp{}", total);
    println!("Subtotal masih terlihat: Rp{}", subtotal);
}

Output yang Dihasilkan:

text
Total: Rp90000
Subtotal masih terlihat: Rp100000

Kode di atas sah dan berjalan benar, tetapi ada kebocoran: variabel subtotal dan diskon tetap hidup sampai akhir main, padahal keduanya hanyalah pijakan menuju total. Baris terakhir membuktikan bahwa subtotal masih bisa diakses jauh setelah kegunaannya habis. Pada program lima baris hal ini sepele; pada program lima ratus baris, variabel perantara seperti inilah yang membuat pembaca kehilangan jejak variabel mana yang benar-benar penting.

Blok { } yang Bernilai #

Solusi Rust atas masalah tersebut memanfaatkan sifat yang sudah kita kenal di Chapter Ekspresi dan Statement: blok adalah expression yang mengevaluasi menjadi nilai baris terakhirnya. Karena blok bernilai, blok boleh ditempatkan di sisi kanan let. Mari kita bungkus ulang perhitungan di atas:

rust
fn main() {
    let harga_satuan = 25000;
    let jumlah = 4;

    let total = {
        let subtotal = harga_satuan * jumlah;
        let diskon = subtotal / 10;
        subtotal - diskon
    };

    println!("Total setelah diskon: Rp{}", total);
}

Output yang Dihasilkan:

text
Total setelah diskon: Rp90000

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 5 (let total = {): Tanda kurung kurawal membuka sebuah blok di posisi yang biasanya diisi nilai. Kompiler memperlakukan seluruh blok ini sebagai satu expression besar yang sedang dievaluasi.
  2. Baris 6 (let subtotal = harga_satuan * jumlah;): Variabel perantara subtotal dibuat — tetapi hanya di dalam scope blok. Variabel ini tidak akan pernah terlihat di luar kurung kurawal.
  3. Baris 7 (let diskon = subtotal / 10;): Variabel perantara kedua, diskon, juga hidup eksklusif di dalam blok.
  4. Baris 8 (subtotal - diskon): Baris terakhir blok ditulis tanpa titik koma, sehingga baris ini adalah nilai kembalian blok. Expression blok mengevaluasi menjadi 90000.
  5. Baris 9 (};): Kurung kurawal menutup blok dan titik koma mengakhiri statement let. Nilai 90000 dari blok terikat ke variabel total. Setelah baris ini, subtotal dan diskon sudah tidak ada.

Hasil akhirnya identik dengan versi rata, tetapi sekarang main hanya mengenal harga_satuan, jumlah, dan total. Detail tahapan perhitungan dikarantina di dalam blok, persis di tempat detail tersebut berarti.

Variabel Perantara Terkunci di Dalam Blok #

Blok bersarang juga bekerja untuk perhitungan yang lebih pendek. Contoh berikut menghitung harga promo dengan satu variabel bantu:

rust
fn main() {
    let harga = 50000;

    let harga_promo = {
        let potongan = 10000;
        harga - potongan
    };

    println!("Harga normal: Rp{}", harga);
    println!("Harga promo: Rp{}", harga_promo);
}

Output yang Dihasilkan:

text
Harga normal: Rp50000
Harga promo: Rp40000

Dua hal patut dicermati dari kode di atas. Pertama, variabel harga yang dideklarasikan di luar blok tetap bisa dibaca di dalam blok — blok bersarang dapat melihat variabel milik scope di luarnya. Kedua, arah sebaliknya tidak berlaku: variabel potongan yang lahir di dalam blok mati bersama kurung kurawal penutup, sehingga baris println! di luar blok sama sekali tidak mengenal potongan. Aturan satu arah inilah yang membuat blok menjadi karantina yang aman: data boleh masuk, variabel sementara tidak boleh keluar.

Menggabungkan Blok dengan Expression if #

Karena if juga expression, kita bisa menyusun perhitungan bertahap yang diakhiri keputusan. Contoh berikut mengevaluasi penjualan terhadap target:

rust
fn main() {
    let penjualan = 150000;
    let target = 100000;

    let evaluasi = {
        let selisih = penjualan - target;
        if selisih >= 0 {
            "Melebihi target"
        } else {
            "Di bawah target"
        }
    };

    println!("Evaluasi: {}", evaluasi);
}

Output yang Dihasilkan:

text
Evaluasi: Melebihi target

Mari kita telaah alur evaluasi blok di atas:

  1. Baris 6 (let selisih = penjualan - target;): Selisih 50000 dihitung sebagai variabel sementara di dalam blok.
  2. Baris 7–11 (if selisih >= 0 { ... } else { ... }): Expression if menjadi baris terakhir blok, sehingga nilai hasil if sekaligus menjadi nilai blok. Karena selisih positif, cabang pertama menang dan blok mengevaluasi menjadi "Melebihi target".
  3. Baris 12 (};): Nilai teks hasil blok diikat ke evaluasi. Variabel selisih lenyap bersama blok.

Ringkasan #

Blok { } adalah expression yang mengevaluasi menjadi nilai baris terakhirnya tanpa titik koma. Menempatkan blok di sisi kanan let memungkinkan kita menghitung bertahap dengan variabel sementara yang mati tepat saat kurung kurawal menutup, sehingga scope luar hanya menerima hasil akhir. Blok dapat membaca variabel luar, dapat berisi statement let maupun expression if, dan patuh pada semua aturan expression yang sudah kita pelajari. Teknik ini akan terus muncul di sepanjang perjalanan kita, dan pola pemahaman scope-nya menjadi tema utama Chapter tentang scope variabel di fungsi.