Lewati ke konten

Scope di Rust

Scope (cakupan) adalah bagian kode tempat sebuah variabel berlaku. Idenya sederhana, tapi kalau kamu memahaminya, banyak aturan Rust lainnya (terutama ownership) jadi jauh lebih masuk akal.

Sebuah variabel ada sejak dideklarasikan sampai tanda } penutup dari blok tempat ia berada:

fn main() {
let x = 5; // x masuk ke scope di sini
{
let y = 10; // y masuk ke scope di sini
println!("x = {x}, y = {y}"); // keduanya berlaku
} // y keluar dari scope di sini
println!("x = {x}");
// println!("{y}"); // ❌ error: y sudah tidak ada di scope
}

Output:

x = 5, y = 10
x = 5
▶ Coba sendiri

y hanya ada di dalam blok { } bagian dalam. Begitu blok itu berakhir, y hilang. Mencoba memakainya setelah itu menghasilkan error saat kompilasi, bukan kejutan saat program berjalan.

Scope hanya berlaku satu arah: blok dalam bisa melihat variabel dari blok-blok di sekitarnya (itulah kenapa blok dalam bisa mencetak x), tapi blok luar tidak bisa melihat ke dalam blok dalam.

Parameter dan variabel apa pun yang kamu buat di dalam sebuah fungsi hanya ada selama pemanggilan fungsi itu:

fn calculate() -> i32 {
let result = 5 * 2; // result hanya ada di dalam calculate
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ error: result tidak ada di sini
}

Output:

10
▶ Coba sendiri

Hal yang sama berlaku sebaliknya: sebuah fungsi tidak bisa melihat variabel lokal milik main. Kalau fungsi itu butuh sebuah nilai, kamu mengirimnya sebagai parameter, seperti yang sudah kamu lihat di halaman Fungsi.

Blok { } tidak hanya membatasi tempat variabel hidup. Blok juga bisa menghasilkan nilai: ekspresi terakhir di dalamnya, yang ditulis tanpa titik koma. Ini aturan yang sama yang membuat sebuah fungsi mengembalikan baris terakhirnya.

Ini berguna ketika kamu butuh beberapa variabel bantu untuk menghitung sesuatu, tapi tidak ingin variabel itu tetap berkeliaran sesudahnya:

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax hanya hidup di dalam blok ini
let shipping = 5;
price + tax + shipping // tanpa titik koma: ini nilai bloknya
};
println!("total = {total}");
// println!("{tax}"); // ❌ error: tax sudah di luar scope
}

Output:

total = 49
▶ Coba sendiri

Ada dua detail yang perlu diperhatikan:

  • Baris terakhir price + tax + shipping tidak memakai titik koma. Kalau kamu menambahkannya, blok itu mengembalikan () (tidak ada apa-apa) alih-alih angka, dan println! tidak bisa mencetaknya.
  • Semuanya adalah satu pernyataan let, jadi diakhiri dengan }; setelah kurung kurawal penutup.

Begitu bloknya berakhir, tax dan shipping hilang. Yang tersisa hanya hasilnya, total. Menaruh variabel bantu di blok kecil seperti ini membuat jelas bahwa variabel itu hanya dibutuhkan untuk satu perhitungan.

Ini berhubungan langsung dengan ownership (kepemilikan), yang akan kamu temui sebentar lagi: sebuah nilai otomatis dibersihkan begitu scope pemiliknya berakhir.

fn main() {
{
let text = String::from("halo");
println!("{text}");
} // scope text berakhir di sini — Rust otomatis membersihkannya (drop)
}

Output:

halo
▶ Coba sendiri

Itulah seluruh mekanisme yang membuat Rust tidak butuh garbage collector: saat kompilasi, Rust sudah tahu persis di mana setiap scope berakhir.

Masih ingat shadowing dari halaman Variabel? Setiap let dengan nama yang sama di dalam sebuah scope membuat variabel yang berbeda. Variabel itu tidak menghapus variabel luar, ia hanya mengambil alih untuk sisa scope tersebut:

fn main() {
let x = 5;
{
let x = x * 2; // x baru, hanya di dalam blok ini
println!("x dalam = {x}"); // 10
}
println!("x luar = {x}"); // 5 — tidak terpengaruh
}

Output:

x dalam = 10
x luar = 5
▶ Coba sendiri

Program ini tidak bisa dikompilasi, karena area dipakai setelah bloknya berakhir:

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("luas = {area}");
}

Ubah supaya program mencetak luas = 12, sementara height tetap hanya hidup di dalam blok. Petunjuk: buat bloknya menghasilkan luas, lalu simpan hasilnya di variabel yang dideklarasikan di luar blok.

Lihat solusi
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("luas = {area}");
}

Output:

luas = 12
▶ Coba sendiri

Sekarang area hidup di scope main, jadi println! bisa melihatnya. height tetap hanya dibutuhkan di dalam blok, dan ia dibersihkan saat bloknya berakhir.

Buat variabel level dengan nilai 1. Buka sebuah blok yang men-shadow-nya dengan 2, dan di dalamnya, blok lain yang men-shadow-nya dengan 3. Cetak level di setiap blok, dan cetak lagi tepat setelah setiap blok ditutup. Petunjuk: kamu seharusnya melihat angkanya naik 1, 2, 3, lalu turun lagi ke 2 dan 1.

Lihat solusi
fn main() {
let level = 1;
println!("level {level}");
{
let level = 2;
println!("level {level}");
{
let level = 3;
println!("level {level}");
}
println!("kembali ke level {level}");
}
println!("kembali ke level {level}");
}

Output:

level 1
level 2
level 3
kembali ke level 2
kembali ke level 1
▶ Coba sendiri

Setiap level di dalam hanya menyembunyikan level di luarnya sampai bloknya berakhir. Variabel-variabel luar tidak pernah diubah, jadi mereka masih ada di sana, menunggu.

  • Scope adalah bagian kode (ditandai { }) tempat sebuah variabel berlaku.
  • Sebuah variabel berhenti ada tepat saat } penutup scope-nya tercapai.
  • Isi fungsi juga sebuah scope: variabel lokal di dalam fungsi hilang saat fungsi itu selesai.
  • Blok adalah ekspresi: baris terakhirnya (tanpa titik koma) adalah nilainya, dan variabel bantunya lenyap saat blok berakhir.
  • Scope-lah yang memberi tahu Rust kapan tepatnya sebuah nilai dibersihkan, makanya tidak perlu garbage collector.

Kuis singkat

1. Menurut halaman ini, kenapa Rust tidak butuh garbage collector?

2. Apa yang terjadi kalau kamu men-shadow sebuah variabel dengan let x = x * 2; di dalam blok dalam?

3. Berapa nilai total setelah let total = { let a = 2; a * 10 };?

Skor: 0 / 3