Smart Pointers — Rust

Smart Pointers di Rust Smart pointer adalah struct yang bertindak seperti pointer tapi punya metadata & kemampuan tambahan. Mereka memungkinkan pola ownership…

Smart pointer adalah nilai yang bertindak seperti penunjuk ke data di tempat lain, tapi sekaligus memiliki data itu dan punya kemampuan tambahan. String dan Vec sebenarnya sudah smart pointer. Pelajaran ini membahas tiga yang paling sering kamu pakai sendiri: Box, Rc, dan RefCell. Bayangin gudang belakang Kedai Martabak Bang Rusdi. Box itu loker: barangnya disimpan di gudang, kasir cukup memegang kuncinya, dan pemegang kunci itulah pemiliknya. Rc itu papan menu yang dipakai dua kasir sekaligus: di pojok papan ada turus yang mencatat berapa orang yang masih memakainya, dan papan baru diturunkan saat orang terakhir selesai. RefCell itu buku stok yang dijaga mandor: siapa pun boleh minta menulis, tapi mandor mengecek di tempat, saat itu juga, supaya gak ada dua orang yang menulis bersamaan.

🧪 Tebakan cepat: stok telur disimpan di RefCell::new(250u8). Kasir 1 mengambil let mut kasir1 = stok_telur.borrow_mut();, lalu sebelum kasir 1 selesai, kasir 2 juga mengambil stok_telur.borrow_mut(). Lolos compile? Kalau lolos, apa yang terjadi saat jalan? Tebak dulu, jawabannya ada di bagian "RefCell: aturan pinjam dicek saat jalan".

Box: data di heap, satu pemilik

fn main() {
    let harga = Box::new(32000u32);
    let total = *harga * 2;
    println!("{} {}", harga, total);
    println!("{}", std::mem::size_of::<Box<u32>>());
}
32000 64000
8

Box dipakai untuk tiga hal: tipe yang ukurannya gak bisa dihitung saat compile (bagian berikut), nilai yang tipenya baru diketahui saat jalan seperti nampan Vec<Box<dyn Harga>> di pelajaran traits (total 107000), dan data besar yang mau dipindah tanpa menyalin isinya.

Tipe rekursif butuh Box

Tumpukan nota di paku: tiap nota berisi harga dan sisa tumpukan di bawahnya.

error[E0072]: recursive type `Tumpukan` has infinite size
 --> src/main.rs:1:1
  |
1 | enum Tumpukan {
  | ^^^^^^^^^^^^^
2 |     Nota(u32, Tumpukan),
  |               -------- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle

Rust harus tahu ukuran tiap tipe saat compile. Nota(u32, Tumpukan) berisi Tumpukan yang berisi Tumpukan lagi, tanpa ujung. Dengan Box, isinya cuma penunjuk 8 byte, dan sisa tumpukannya di heap:

use Tumpukan::{Kosong, Nota};

enum Tumpukan {
    Nota(u32, Box<Tumpukan>),
    Kosong,
}

fn total(t: &Tumpukan) -> u32 {
    match t {
        Nota(harga, sisa) => harga + total(sisa),
        Kosong => 0,
    }
}

fn main() {
    let tumpukan = Nota(102000, Box::new(Nota(70000, Box::new(Nota(38000, Box::new(Kosong))))));
    println!("{}", total(&tumpukan));
}

Output-nya 210000. use Tumpukan::{Kosong, Nota}; membuat variant-nya bisa ditulis tanpa Tumpukan::.

Rc: banyak pemilik, satu data

Kadang satu data memang dimiliki beberapa pihak, dan gak jelas siapa yang terakhir selesai. Rc<T> (reference counted) menyimpan datanya sekali dan menghitung pemiliknya:

use std::rc::Rc;

struct Menu {
    nama: String,
}

impl Drop for Menu {
    fn drop(&mut self) {
        println!("{} diturunkan", self.nama);
    }
}

fn main() {
    let menu = Rc::new(Menu { nama: String::from("Papan menu") });
    println!("pemilik: {}", Rc::strong_count(&menu));
    let kasir1 = Rc::clone(&menu);
    {
        let kasir2 = Rc::clone(&menu);
        println!("pemilik: {}", Rc::strong_count(&menu));
        println!("kasir2 membaca {}", kasir2.nama);
    }
    println!("pemilik: {}", Rc::strong_count(&menu));
    drop(menu);
    println!("pemilik: {}", Rc::strong_count(&kasir1));
    println!("kasir1 membaca {}", kasir1.nama);
}
pemilik: 1
pemilik: 3
kasir2 membaca Papan menu
pemilik: 2
pemilik: 1
kasir1 membaca Papan menu
Papan menu diturunkan

RefCell: aturan pinjam dicek saat jalan

use std::cell::RefCell;

fn main() {
    let stok_telur = RefCell::new(250u8);
    *stok_telur.borrow_mut() -= 2;
    println!("stok: {}", stok_telur.borrow());
}

Output-nya stok: 248. stok_telur gak mut, tapi isinya berubah. Ini disebut interior mutability: borrow_mut() memberi pinjaman untuk menulis, borrow() pinjaman untuk membaca. Aturannya sama dengan pelajaran borrowing (banyak pembaca atau satu penulis), bedanya dicek saat program jalan, bukan saat compile. Ini kode tebakan tadi:

use std::cell::RefCell;

fn main() {
    let stok_telur = RefCell::new(250u8);
    let mut kasir1 = stok_telur.borrow_mut();
    *kasir1 -= 2;
    let mut kasir2 = stok_telur.borrow_mut();
    *kasir2 -= 2;
    println!("stok: {}", stok_telur.borrow());
}

thread 'main' (25675696) panicked at src/main.rs:7:33:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Lolos compile, lalu panic di baris 7. Itu jawaban tebakannya. Compiler gak bisa melihat isi RefCell, jadi ia percaya; mandornya baru memeriksa saat borrow_mut() kedua dipanggil, dan kasir1 masih memegang pinjamannya. Kebalikannya, borrow() saat ada penulis, memberi RefCell already mutably borrowed. Pinjaman dari RefCell hidup sampai variabelnya keluar scope, jadi simpan sesingkat mungkin: *stok_telur.borrow_mut() -= 2; dalam satu baris langsung selesai. Kalau mau mengecek tanpa panic, stok_telur.try_borrow_mut() memberi Err(BorrowMutError).

Rc<RefCell>: stok bersama yang bisa diubah

Rc memberi banyak pemilik, RefCell memberi izin menulis. Digabung, dua kasir bisa memegang dan mengubah stok yang sama:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let stok_telur = Rc::new(RefCell::new(250u8));
    let kasir1 = Rc::clone(&stok_telur);
    let kasir2 = Rc::clone(&stok_telur);
    *kasir1.borrow_mut() -= 2;
    *kasir2.borrow_mut() -= 2;
    println!("stok telur: {}", stok_telur.borrow());
    println!("pemilik: {}", Rc::strong_count(&stok_telur));
}
stok telur: 246
pemilik: 3

Tiap kasir menjual satu martabak telur yang memakai 2 butir, dan keduanya melihat angka yang sama: 250, 248, lalu 246. Pola ini juga dipakai untuk struktur seperti graf, di mana satu node dimiliki banyak edge: node-nya Rc<T>, atau Rc<RefCell<T>> kalau node-nya perlu diubah.

Weak, sekilas

Kalau dua Rc saling menunjuk (misalnya pesanan menunjuk pelanggannya dan pelanggan menunjuk pesanannya), hitungannya gak pernah turun ke 0, dan datanya gak pernah dibuang: memory leak. Weak<T> adalah penunjuk yang gak ikut dihitung sebagai pemilik. Rc::downgrade(&menu) membuatnya: Rc::strong_count tetap 1 dan Rc::weak_count jadi 1. lirik.upgrade() memberi Some(...) selama datanya masih ada, dan None setelah pemilik terakhir membuangnya. Pakai Weak untuk arah "balik", seperti anak yang menunjuk induknya.

Arc dan Mutex untuk thread, sekilas

Rc menghitung pemilik dengan cara yang gak aman kalau dua thread mengubah hitungannya bersamaan, jadi compiler menolak mengirimnya ke thread lain: error[E0277]: `Rc<u8>` cannot be sent between threads safely. Versi untuk thread adalah Arc<T> (atomic reference counted), dan pasangan RefCell-nya adalah Mutex<T>: kunci yang membuat thread lain menunggu, bukan panic.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let stok_telur = Arc::new(Mutex::new(250u8));
    let mut dapur = Vec::new();
    for _ in 0..2 {
        let stok = Arc::clone(&stok_telur);
        dapur.push(thread::spawn(move || {
            *stok.lock().unwrap() -= 2;
        }));
    }
    for d in dapur {
        d.join().unwrap();
    }
    println!("stok telur: {}", stok_telur.lock().unwrap());
}

Output-nya stok telur: 246. Dua thread dapur masing-masing memegang Arc sendiri (lewat move, dari pelajaran closures), dan lock() memastikan cuma satu yang menulis pada satu waktu. Thread dibahas lebih dalam di luar track ini.

Memilih smart pointer

Kebutuhan Pakai
Data di heap, satu pemilik (tipe rekursif, dyn Trait) Box<T>
Banyak pemilik, satu thread, cuma dibaca Rc<T>
Banyak pemilik, banyak thread, cuma dibaca Arc<T>
Mengubah lewat pinjaman biasa, satu thread RefCell<T>
Banyak pemilik dan bisa diubah, satu thread Rc<RefCell<T>>
Banyak pemilik dan bisa diubah, banyak thread Arc<Mutex<T>>

⚠️ Jebakan yang sering ketemu

TL;DR: Box<T> menaruh data di heap dengan satu pemilik, penunjuknya 8 byte; dipakai untuk tipe rekursif (E0072 tanpanya) dan dyn Trait. Rc<T> memberi banyak pemilik di satu thread: Rc::clone(&data) menambah hitungan, Rc::strong_count(&data) membacanya (1, 2, 3), dan datanya dibuang saat pemilik terakhir selesai; isinya cuma bisa dibaca. RefCell<T> memindahkan aturan pinjam ke saat program jalan: borrow() dan borrow_mut(), dan pelanggarannya panic RefCell already borrowed. Rc<RefCell<T>> untuk data bersama yang bisa diubah, Weak untuk memutus lingkaran, dan untuk banyak thread: Arc<T> dan Arc<Mutex<T>>.

Yang akan kamu pelajari