Saat mengolah data, kita sering mengubah bentuknya dari tahap ke tahap: teks hasil input diubah menjadi angka, angka dikalikan pajak, lalu hasilnya dibulatkan. Di PHP, kita cenderung menimpa variable yang sama ($harga = (int) $harga;). Chapter sebelumnya mengajarkan bahwa mut hanya mengizinkan penimpaan nilai bertipe sama, sehingga pola “nilai berubah bentuk, nama tetap” butuh mekanisme lain. Rust menjawab kebutuhan ini dengan shadowing: mendeklarasikan ulang variable bernama sama menggunakan let baru, sehingga variable lama tertutup oleh variable baru. Pada Chapter ini kita akan memakai shadowing untuk transformasi bertahap, memahami interaksinya dengan scope, dan membandingkannya dengan mut.

Cara Kerja Shadowing #

Shadowing terjadi ketika let baru memakai nama yang sudah dipakai variable sebelumnya di scope yang sama. Variable baru sepenuhnya terpisah dari variable lama: ia boleh bertipe berbeda, dan keberadaannya menutup (shadow) akses ke variable lama di titik tersebut ke bawah. Variable lama tetap ada di memori sampai scope berakhir, tetapi namanya sudah tidak bisa kita pakai lagi karena setiap penulisan nama itu kini mengarah ke variable baru.

Bentuk paling umum shadowing adalah mengubah tipe nilai dengan nama yang tetap, misalnya data kode pos yang awalnya berupa teks lalu diubah menjadi bilangan:

rust
fn main() {
    let kode_pos = "40115";
    println!("Kode pos (teks): {}", kode_pos);

    let kode_pos = 40115;
    println!("Kode pos (angka): {}", kode_pos);
}

Output yang Dihasilkan:

text
Kode pos (teks): 40115
Kode pos (angka): 40115

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

  1. Baris 2 (let kode_pos = "40115";): variable kode_pos pertama lahir sebagai teks (&str) berisi "40115"; tanda kutip ganda menandakan tipe string.
  2. Baris 4 (let kode_pos = 40115;): let baru mendeklarasikan variable kedua bernama sama; variable kedua ini bertipe bilangan bulat i32 dan kini membayangi variable pertama.
  3. Baris 5 (println!("Kode pos (angka): {}", kode_pos);): nama kode_pos di baris ini mengarah ke variable kedua, sehingga tercetak 40115 sebagai angka, bukan teks.

Kedua println! mencetak tampilan yang mirip, tetapi tipe datanya berbeda: variable pertama menyimpan teks, variable kedua menyimpan bilangan yang siap dihitung. Perubahan tipe seperti ini mustahil dilakukan lewat mut, karena mut mengikat tipe sejak deklarasi pertama.

Transformasi Nilai secara Bertahap #

Kekuatan shadowing terasa ketika satu nilai melewati beberapa tahap pengolahan. Setiap tahap menghasilkan variable baru dengan nama sama, sehingga pembaca kode melihat satu nama yang nilainya “mengeras” dari baris ke baris. Contoh berikut mengolah panjang tanah yang digandakan lalu ditambah lebar jalan:

rust
fn main() {
    let panjang_tanah = 12;
    println!("Panjang awal: {} meter", panjang_tanah);

    let panjang_tanah = panjang_tanah * 2;
    println!("Setelah digandakan: {} meter", panjang_tanah);

    let panjang_tanah = panjang_tanah + 3;
    println!("Panjang final: {} meter", panjang_tanah);
}

Output yang Dihasilkan:

text
Panjang awal: 12 meter
Setelah digandakan: 24 meter
Panjang final: 27 meter

Mari kita bedah prosesnya:

  1. Baris 2 (let panjang_tanah = 12;): variable pertama menyimpan nilai 12.
  2. Baris 5 (let panjang_tanah = panjang_tanah * 2;): sisi kanan membaca variable lama bernilai 12, mengalikannya dengan 2, lalu let baru mengikat hasil 24 pada nama yang sama.
  3. Baris 8 (let panjang_tanah = panjang_tanah + 3;): variable tahap kedua bernilai 24 dijumlah 3 menjadi 27, lalu diikat oleh let ketiga; nilai 12 dan 24 sebelumnya sudah tertutup.

Setiap tahap bersifat immutable: tidak ada penimpaan, hanya lahirnya variable baru. Jika suatu tahap salah menghitung, kita bisa menghapus let pada tahap itu tanpa merusak tahap lain, karena setiap tahap berdiri di atas hasil tahap sebelumnya.

Shadowing dan Scope #

Scope variable di Rust dibatasi oleh blok kode tempat ia dideklarasikan, ditandai pasangan tanda kurung kurawal { }. Shadowing di dalam blok hanya berlaku sampai blok ditutup; setelah itu, variable di scope luar muncul kembali dengan nilai yang tidak tersentuh:

rust
fn main() {
    let stok = 100;
    println!("Stok luar: {}", stok);

    {
        let stok = 50;
        println!("Stok dalam blok: {}", stok);
    }

    println!("Stok setelah blok: {}", stok);
}

Output yang Dihasilkan:

text
Stok luar: 100
Stok dalam blok: 50
Stok setelah blok: 100

Mari kita amati alurnya langkah demi langkah:

  1. Baris 2 (let stok = 100;): variable stok milik scope fungsi main lahir dengan nilai 100.
  2. Baris 6 (let stok = 50;): let ini berada di dalam blok baru, sehingga variable stok kedua hanya hidup di dalam blok dan membayangi stok lama selama blok berjalan.
  3. Baris 8 (}): blok ditutup dan variable stok bernilai 50 musnah bersama scope-nya.
  4. Baris 10 (println!("Stok setelah blok: {}", stok);): nama stok kini kembali mengacu pada variable luar yang tetap bernilai 100.

Pola ini sering dipakai untuk perhitungan sementara: blok kecil menghitung versi data yang berbeda tanpa mencemari variable di scope luar.

Shadowing versus mut #

Kedua mekanisme sama-sama membuat nama variable memegang nilai berbeda, tetapi karakternya bertolak belakang. Kode berikut mencoba mengubah tipe variable mutable dan ditolak keras oleh kompilator:

rust
fn main() {
    let mut berat = 72;
    berat = 72.5;

    println!("Berat: {} kg", berat);
}

Pesan galatnya berbunyi:

text
error[E0308]: mismatched types
 --> main.rs:3:13
  |
2 |     let mut berat = 72;
  |                     -- expected due to this value
3 |     berat = 72.5;
  |             ^^^^ expected integer, found floating-point number

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Kompilator menjelaskan bahwa tipe berat terkunci sebagai integer sejak 72 diberikan pada baris 2, sehingga 72.5 yang bertipe floating point ditolak. Tabel berikut merangkum perbedaan keduanya:

AspekmutShadowing
Cara memakaisekali di deklarasi, lalu ditimpa (berat = 80;)let baru setiap kali nilai berubah
Tipe datawajib tetap samaboleh berbeda setiap let
Nilai lamatertimpa dan hilangmasih ada di memori, hanya tertutup
Kapan dipilihnilai benar-benar berubah (counter, saldo)nilai berubah bentuk atau tahap (input → angka → hasil)

Ringkasan #

Shadowing mendeklarasikan ulang variable bernama sama melalui let baru; variable baru membayangi variable lama, boleh bertipe berbeda, dan berumur sesuai scope tempat ia dideklarasikan. Pola ini ideal untuk transformasi bertahap yang mengubah bentuk data tanpa menumpuk nama perantara. Sebaliknya, mut tetap pilihan tepat untuk keadaan yang berubah dengan tipe yang tetap. Selanjutnya kita akan mengenal konstanta, kerabat variable yang nilainya dipatok sejak program dikompilasi.