Ownership adalah cara Rust mengurus memori tanpa garbage collector: tiap nilai punya tepat satu pemilik (owner), yaitu satu variabel, dan saat pemiliknya selesai, nilainya dibuang. Bayangin nota pesanan di Kedai Martabak Bang Rusdi. Notanya cuma selembar. Kalau kasir menyerahkannya ke dapur, kasir gak memegangnya lagi; mau tetap pegang juga, ia harus memfotokopinya dulu, dan itu makan kertas dan waktu. Sebaliknya, harga 32000 cukup diteriakkan: semua orang bisa tahu angkanya tanpa ada yang kehilangan apa-apa. Waktu kedai tutup, tiap orang membuang nota yang masih ia pegang, jadi gak ada nota yang dibuang dua kali. Nota itu String, menyerahkan itu move, fotokopi itu clone, angka yang diteriakkan itu copy, dan membuang saat tutup itu drop.
🧪 Tebakan cepat: nota Sari dibuat dulu, lalu nota Budi di dalam blok
{ }, lalu nota Rina. Tiap nota mencetakNota ... dibuangsaat dibuang. Urutan ketiga pesan itu bagaimana? Tebak dulu, jawabannya ada di bagian "Drop: dibuang saat scope berakhir".
Tiga aturan ownership
- Tiap nilai punya satu pemilik: variabel yang menyimpannya.
- Pada satu waktu, pemiliknya cuma satu.
- Saat pemiliknya keluar dari scope, nilainya di-drop (dibuang dari memori).
Scope adalah wilayah sebuah variabel hidup: dari baris let-nya sampai kurung kurawal } yang menutup bloknya. Ketiga aturan ini diperiksa borrow checker saat compile, jadi gak ada biaya tambahan saat program jalan.
Move: pemiliknya pindah
fn main() {
let nota = String::from("2x martabak manis");
let dapur = nota;
println!("Dapur: {}", dapur);
println!("Kasir: {}", nota);
}
error[E0382]: borrow of moved value: `nota`
--> src/main.rs:5:27
|
2 | let nota = String::from("2x martabak manis");
| ---- move occurs because `nota` has type `String`, which does not implement the `Copy` trait
3 | let dapur = nota;
| ---- value moved here
4 | println!("Dapur: {}", dapur);
5 | println!("Kasir: {}", nota);
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let dapur = nota.clone();
| ++++++++
let dapur = nota; memindahkan pemiliknya ke dapur (aturan 2), jadi nota gak valid lagi, dan memakainya ditolak saat compile. Ini error yang sama dengan yang kamu lihat di pelajaran pertama, di String, dan di Vec. Sekarang kita lihat kenapa.
Stack, heap, dan kenapa String dipindah
Program menyimpan data di dua tempat. Stack untuk nilai yang ukurannya sudah pasti saat compile: cepat, dan otomatis bersih saat fungsinya selesai. Heap untuk data yang ukurannya bisa berubah saat jalan, seperti teks yang bisa ditambah: tempatnya dipesan (allocate) saat dibutuhkan dan harus dikembalikan kalau sudah selesai.
use std::mem::size_of;
fn main() {
let nota = String::from("2x martabak manis");
println!("{} {}", nota.len(), nota.capacity());
println!("String: {}", size_of::<String>());
println!("Vec<u32>: {}", size_of::<Vec<u32>>());
println!("u32: {}", size_of::<u32>());
println!("&str: {}", size_of::<&str>());
println!("ptr: {:p}", nota.as_ptr());
let dapur = nota;
println!("ptr: {:p}", dapur.as_ptr());
let salinan = dapur.clone();
println!("ptr: {:p}", salinan.as_ptr());
}
17 17
String: 24
Vec<u32>: 24
u32: 4
&str: 16
ptr: 0x105009f00
ptr: 0x105009f00
ptr: 0x105009f60
- Di komputer 64-bit, sebuah
Stringselalu 24 byte di stack: tiga angka 8 byte, yaitu alamat isinya di heap (pointer), panjang (len, 17), dan kapasitas (capacity, 17). Ke-17 huruf2x martabak manissendiri ada di heap. u32cuma 4 byte, langsung di stack. Gak ada apa-apa di heap.- Alamat (
{:p}) di komputermu pasti beda, dan berubah tiap kali jalan. Yang penting polanya:notadandapurmenunjuk alamat yang sama,salinanalamat yang beda.
Jadi move cuma menyalin 24 byte di stack; isi di heap gak disentuh. Kalau sesudah itu nota dan dapur sama-sama valid, saat main selesai keduanya akan mengembalikan 17 byte heap yang sama: double free, bug yang bisa merusak memori dan sering jadi celah keamanan di C dan C++. Rust mencegahnya dengan aturan 2: sesudah move, cuma dapur pemiliknya, cuma dapur yang membuangnya, dan nota gak boleh dipakai.
Copy dan clone
fn main() {
let harga: u32 = 32000;
let salinan = harga;
println!("{} {}", harga, salinan);
let nota = String::from("2x martabak manis");
let dapur = nota.clone();
println!("Kasir: {}", nota);
println!("Dapur: {}", dapur);
}
32000 32000
Kasir: 2x martabak manis
Dapur: 2x martabak manis
- Tipe yang seluruh isinya di stack, seperti
u32,i32,f64,bool, danchar, punya traitCopy:let salinan = harga;menyalin 4 byte-nya, danhargatetap valid. Gak ada heap, jadi gak ada yang bisa dibuang dua kali. StringdanVecgak punyaCopy..clone()membuat salinan penuh: memesan tempat baru di heap lalu menyalin semua byte-nya (alamat0x105009f60tadi). Hasilnya dua pemilik, dua isi, masing-masing dibuang sendiri.- Clone itu ongkos: untuk nota 17 byte gak terasa, untuk Vec berisi sejuta pesanan terasa. Pakai kalau memang butuh dua salinan. Kalau cuma perlu membaca, ada cara yang lebih murah di pelajaran borrowing.
Ownership dan fungsi
Memberi nilai ke fungsi juga move. Parameter fungsi jadi pemilik barunya:
fn masak(nota: String) {
println!("Dimasak: {}", nota);
}
fn main() {
let nota = String::from("2x martabak manis");
masak(nota);
println!("Kasir: {}", nota);
}
Baris terakhir ditolak dengan borrow of moved value: `nota` , dan compiler menambah catatan consider changing this parameter type in function `masak` to borrow instead if owning the value isn't necessary. Satu cara memakai notanya lagi: fungsinya mengembalikan nota itu, jadi pemiliknya pindah balik:
fn masak(nota: String) -> String {
println!("Dimasak: {}", nota);
nota
}
fn main() {
let nota = String::from("2x martabak manis");
let nota = masak(nota);
println!("Kembali ke kasir: {}", nota);
}
Output-nya Dimasak: 2x martabak manis lalu Kembali ke kasir: 2x martabak manis. Bolak-balik begini cepat melelahkan. Cara yang biasa dipakai adalah meminjamkan nota tanpa menyerahkannya, ¬a, yang jadi bahan pelajaran berikutnya: borrowing.
Drop: dibuang saat scope berakhir
Trait Drop membuat kita bisa melihat kapan sebuah nilai dibuang. Rust memanggil drop otomatis; kamu gak pernah memanggilnya sendiri.
struct Nota {
isi: String,
}
impl Drop for Nota {
fn drop(&mut self) {
println!("Nota {} dibuang", self.isi);
}
}
fn main() {
let sari = Nota { isi: String::from("Sari") };
{
let budi = Nota { isi: String::from("Budi") };
println!("Melayani {}", budi.isi);
}
let rina = Nota { isi: String::from("Rina") };
println!("Melayani {} dan {}", sari.isi, rina.isi);
}
Melayani Budi
Nota Budi dibuang
Melayani Sari dan Rina
Nota Rina dibuang
Nota Sari dibuang
Itu jawaban tebakannya: Budi, lalu Rina, lalu Sari. budi dibuang begitu blok dalamnya ditutup }, sebelum Rina dibuat. Di akhir main, variabel dibuang terbalik dari urutan dibuatnya: rina (terakhir dibuat) duluan, sari (pertama dibuat) paling akhir, seperti tumpukan piring. Kalau sari dipindah ke fungsi seperti masak(sari), pesannya muncul di akhir masak, karena pemiliknya sekarang parameter fungsi itu.
⚠️ Jebakan yang sering ketemu
- Memakai variabel setelah move.
let dapur = nota;ataumasak(nota)lalu memakainota: E0382borrow of moved value. - Move di dalam loop.
let dapur = nota;di dalamforditolak denganvalue moved here, in previous iteration of loop: putaran kedua gak punya nota lagi. - Clone di mana-mana supaya lolos compile. Lolos, tapi tiap clone memesan heap dan menyalin semua byte. Biasanya yang dibutuhkan cuma meminjam (
&). - Mengira u32 juga dipindah. Tipe
Copydisalin, jadi aslinya tetap valid. Yang dipindah adalah tipe yang punya data di heap, sepertiString,Vec, dan struct yang berisi keduanya.
TL;DR: Tiap nilai punya satu pemilik, dan saat pemiliknya keluar scope, nilainya di-drop. let dapur = nota; dan masak(nota) memindahkan pemiliknya, jadi nota gak boleh dipakai lagi (E0382). Move cuma menyalin 24 byte String di stack (pointer, len, capacity), isinya di heap tetap satu, dan aturan satu pemilik inilah yang mencegah double free. Tipe Copy seperti u32 (4 byte, tanpa heap) disalin, bukan dipindah. .clone() menyalin isi heap juga, jadi ada ongkosnya. Fungsi bisa mengembalikan ownership dengan me-return nilainya. Drop terjadi otomatis di akhir scope, terbalik dari urutan dibuat. Berikutnya: meminjam dengan & tanpa memindahkan.