Ownership — Rust

Ownership di Rust Ownership adalah fitur paling unik di Rust — inilah yang memungkinkan Rust menjamin memory safety tanpa garbage collector. Konsep ini tidak…

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 mencetak Nota ... dibuang saat dibuang. Urutan ketiga pesan itu bagaimana? Tebak dulu, jawabannya ada di bagian "Drop: dibuang saat scope berakhir".

Tiga aturan ownership

  1. Tiap nilai punya satu pemilik: variabel yang menyimpannya.
  2. Pada satu waktu, pemiliknya cuma satu.
  3. 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

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

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, &nota, 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

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.

Yang akan kamu pelajari