Bahasa pemrograman mengelola memori dengan cara yang berbeda-beda: C menyerahkan tanggung jawab penuh kepada programmer, sementara Java dan Go memakai garbage collector yang membersihkan memori di latar belakang. Rust memilih jalan ketiga yang lebih tegas: seluruh memori dikelola lewat sekumpulan aturan bernama ownership yang diperiksa oleh kompilator pada saat kompilasi. Tidak ada biaya runtime untuk pemeriksaan ini — program hasil kompilasi berjalan secepat kode C, tetapi sekelas galat memori yang lazim di C dicegah sebelum program sempat berjalan. Pada Chapter ini kita akan membedah tiga aturan ownership yang menjadi fondasi seluruh sistem tersebut.
Tiga Aturan Ownership #
Seluruh perilaku ownership di Rust dapat diturunkan dari tiga aturan sederhana berikut:
- Setiap nilai di Rust memiliki tepat satu pemilik (owner).
- Satu nilai hanya boleh dimiliki oleh satu pemilik pada satu waktu.
- Ketika pemilik keluar dari scope, nilai dijatuhkan (drop) dari memori.
Aturan pertama memastikan tidak ada data yatim yang tidak jelas penanggung jawabnya. Aturan kedua memastikan tidak ada data yang diklaim oleh dua pemilik sekaligus. Aturan ketiga menutup celah kebocoran memori: begitu pemiliknya keluar scope, Rust otomatis menjalankan pembersihan tanpa menunggu garbage collector. Ketiga aturan tersebut terdengar sederhana, tetapi konsekuensinya menjalar ke semua aspek bahasa — mulai dari perilaku assignment, pemanggilan fungsi, hingga borrowing yang akan kita pelajari di paruh kedua section ini.
Scope Menentukan Umur Sebuah Nilai #
Kita sudah melihat di Chapter sebelumnya bahwa kurung kurawal { } menentukan scope sebuah variabel. Ownership memakai batas yang sama untuk menentukan umur sebuah nilai: nilai dianggap sah mulai dari titik variabel dideklarasikan sampai scope pemiliknya berakhir. Ketika eksekusi program melewati kurung tutup, Rust menjalankan pembersihan otomatis — istilah teknisnya, nilai tersebut di-drop.
Untuk data yang hidup di stack seperti i32, proses drop hanya berarti melepas slot dari tumpukan stack. Untuk data yang menyewa ruang di heap seperti String, drop berarti Rust mengembalikan ruang sewa tersebut ke allocator. Programmer tidak perlu memanggil free atau delete secara manual seperti di C dan C++; kompilator menyisipkan panggilan pembersihan tersebut pada posisi yang tepat.
Melihat Proses Drop Secara Langsung #
Proses drop biasanya tidak terlihat karena berlangsung senyap. Supaya kita bisa mengamatinya, kita akan sengaja membuat tipe kecil bernama Task yang mencetak pesan setiap kali nilainya di-drop. Caranya adalah mengimplementasikan trait Drop — sebuah hook yang dipanggil Rust pada saat nilai dijatuhkan. Untuk sementara, perlakukan struct dan impl di bawah ini sebagai alat peraga; pembahasan mendalam tentang keduanya menyusul di section Struktur Data.
struct Task {
name: String,
}
impl Drop for Task {
fn drop(&mut self) {
println!(">> Tugas '{}' dihapus dari memori", self.name);
}
}
fn main() {
println!("Program dimulai");
{
let task_a = Task { name: String::from("Validasi form") };
let task_b = Task { name: String::from("Kirim email") };
println!("Tugas aktif: {} dan {}", task_a.name, task_b.name);
}
println!("Blok selesai, kedua tugas sudah tidak ada");
}Output yang Dihasilkan:
Program dimulai
Tugas aktif: Validasi form dan Kirim email
>> Tugas 'Kirim email' dihapus dari memori
>> Tugas 'Validasi form' dihapus dari memori
Blok selesai, kedua tugas sudah tidak adaMari kita bedah kode dan urutan output di atas:
- Baris 1–3 (
struct Task ...): kita mendefinisikan tipeTaskyang menyimpan satu fieldnamebertipeString. SetiapTaskadalah nilai yang memiliki data heap di dalamnya. - Baris 5–9 (
impl Drop for Task ...): blok ini memberitahu Rust agar menjalankan fungsidropsetiap kali sebuahTaskdijatuhkan. Fungsi tersebut hanya mencetak nama tugas, sehingga momen pembersihan menjadi terlihat. - Baris 15 (
let task_a = ...):task_amenjadi pemilik pertama sebuah nilaiTaskbernama “Validasi form”. Sejak baris ini, aturan ketiga mulai berlaku bagitask_a. - Baris 16 (
let task_b = ...):task_bmenjadi pemilik nilaiTaskkedua bernama “Kirim email”. Kedua nilai ini saling independen — masing-masing punya pemilik sendiri. - Baris 17 (
println! ...): kedua tugas masih hidup di dalam scope, sehingga namanya dapat dicetak. - Baris 18 (
}): scope blok berakhir. Rust menjatuhkan semua nilai yang dimiliki di dalam scope ini, dan urutannya kebalikan dari urutan pembuatan — “Kirim email” dihapus lebih dulu, baru “Validasi form”, persis seperti pola LIFO pada stack yang kita bahas di Chapter sebelumnya. - Baris 20 (
println! ...): baris ini terbukti berjalan setelah kedua proses drop selesai, karena outputnya muncul paling akhir.
Urutan drop yang terbalik bukan kebetulan. Nilai yang dibuat belakangan mungkin bergantung pada nilai yang dibuat lebih dulu, sehingga membersihkan yang baru lebih dulu lebih aman. Perilaku deterministik seperti ini adalah ciri khas Rust: kapan dan dalam urutan apa memori dibersihkan dapat kita prediksi hanya dengan membaca struktur kode.
Mengakses Nilai di Luar Scope #
Aturan ketiga juga bekerja sebagai pagar. Sebuah variabel hanya sah dari deklarasinya sampai akhir scope-nya, dan kompilator menolak keras upaya pemakaian di luar pagar tersebut. Kode berikut sengaja kita tulis salah untuk melihat bentuk penolakannya — kode ini tidak dapat dikompilasi:
fn main() {
{
let voucher_code = String::from("RUST2025");
println!("Voucher: {}", voucher_code);
}
println!("Voucher: {}", voucher_code);
}Simpan kode di atas sebagai main.rs lalu jalankan rustc --edition 2021 main.rs; kompilasi akan gagal dengan pesan berikut.
Pesan error kompilator:
error[E0425]: cannot find value `voucher_code` in this scope
--> main.rs:7:29
|
7 | println!("Voucher: {}", voucher_code);
| ^^^^^^^^^^^^
|
help: the binding `voucher_code` is available in a different scope in the same function
--> main.rs:3:13
|
3 | let voucher_code = String::from("RUST2025");
| ^^^^^^^^^^^^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0425`.Pesan E0425 di atas memberi tahu dua hal. Pertama, kompilator tidak menemukan voucher_code pada baris 7 karena variabel tersebut sudah di-drop bersama penutupan scope pada baris 5. Kedua, kompilator membantu menjelaskan bahwa binding voucher_code memang ada — tetapi hidup di scope yang berbeda. Galat seperti ini akan tertangkap di banyak bahasa, termasuk PHP; yang membedakan Rust adalah penegakan scope yang sama juga menjadi dasar penegakan aturan ownership yang jauh lebih ketat di bab-bab selanjutnya.
Satu Pemilik pada Satu Waktu #
Aturan kedua menimbulkan pertanyaan yang wajar: apa yang terjadi jika kita menuliskan let other = voucher_code; di dalam scope yang sah — apakah kini ada dua pemilik? Jawabannya: tidak. Untuk tipe seperti String, penugasan semacam itu memindahkan kepemilikan dari variabel lama ke variabel baru, dan variabel lama langsung kehilangan statusnya sebagai pemilik. Mekanisme perpindahan ini bernama move dan menjadi fokus Chapter berikutnya.
Ringkasan #
Ownership adalah kontrak tiga aturan: satu nilai satu pemilik, satu pemilik pada satu waktu, dan nilai di-drop saat pemiliknya keluar scope. Kita telah melihat proses drop berlangsung langsung lewat trait Drop, mengamati urutan pembersihan yang terbalik dari urutan pembuatan, dan menyaksikan kompilator menolak akses ke variabel yang sudah melewati akhir scope-nya. Pertanyaan berikutnya: bagaimana kepemilikan berpindah dari satu variabel ke variabel lain? Jawabannya ada pada move semantics.