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 mengambillet mut kasir1 = stok_telur.borrow_mut();, lalu sebelum kasir 1 selesai, kasir 2 juga mengambilstok_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::new(32000u32)menaruh angkanya di heap. Yang ada di stack cuma penunjuknya, 8 byte.*hargamembaca nilai di dalam Box (dereference, seperti di pelajaran borrowing).println!membukanya sendiri.- Box punya satu pemilik, persis seperti
String: move memindahkan kuncinya, dan saat pemiliknya keluar scope, isinya di heap ikut dibuang.
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
Rc::clone(&menu)gak menyalin papannya. Ia cuma menambah satu pemilik: hitungannya naik dari 1 ke 2, lalu ke 3. MenulisRc::clone(&menu), bukanmenu.clone(), membuat jelas bahwa ini murah.Rc::strong_count(&menu)membaca jumlah pemiliknya.kasir2keluar scope di}, jadi turun ke 2.drop(menu)membuang satu pemilik lagi, jadi 1.- Papannya baru dibuang saat pemilik terakhir selesai:
Papan menu diturunkantercetak paling akhir, setelahkasir1habis di akhirmain. - Data di dalam Rc cuma bisa dibaca.
*kasir1 -= 2padaRc<u8>ditolak dengan E0594cannot assign to data in an `Rc`: kalau banyak pemilik boleh menulis sembarangan, aturan borrowing jebol. - Rc cuma untuk satu thread. Untuk banyak thread ada
Arc, di bagian terakhir.
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
- Tipe rekursif tanpa Box. E0072
recursive type ... has infinite size. Bungkus bagian yang berulang:Box<Tumpukan>. - Mengubah lewat Rc. E0594
cannot assign to data in an `Rc`atau E0596 untuk method sepertipush_str. PakaiRc<RefCell<T>>. - Menahan
borrow_mut()terlalu lama. Lolos compile, lalu panicRefCell already borrowedsaat jalan. Pinjam, ubah, selesai di satu baris. - Rc ke thread lain. E0277
`Rc<u8>` cannot be sent between threads safely. PakaiArc, danArc<Mutex<T>>kalau perlu diubah. - Rc yang saling menunjuk. Gak pernah dibuang. Jadikan salah satu arahnya
Weak.
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>>.