Lewati ke konten

Smart Pointer di Rust

Aturan ownership (kepemilikan) dan borrowing (peminjaman) sudah mencakup sebagian besar situasi. Tapi ada beberapa kasus nyata yang tidak pas dengan aturan “tepat satu pemilik”: nilai yang ukurannya baru diketahui saat program berjalan, data yang memang butuh lebih dari satu pemilik, atau nilai yang perlu diubah lewat referensi bersama. Smart pointer (pointer pintar) adalah jawaban Rust untuk masing-masing kasus itu.

Secara bawaan, nilai disimpan di stack, dan compiler perlu tahu ukurannya sejak awal. Box<T> memindahkan nilai ke heap dan memberimu pointer berukuran tetap yang menunjuk ke sana. Ini berguna saat ukuran sebuah tipe tidak bisa diketahui saat kompilasi, paling sering pada tipe rekursif:

fn main() {
let boxed = Box::new(5);
println!("{}", *boxed); // * untuk mengambil nilainya
}

Output:

5
▶ Coba sendiri

Contoh klasik saat kamu butuh Box: tipe yang berisi dirinya sendiri, seperti node pada linked list:

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list {
List::Cons(value, _) => println!("Nilai pertama: {value}"),
List::Nil => println!("Kosong"),
}
}

Output:

Nilai pertama: 1
▶ Coba sendiri

Tanpa Box, Cons(i32, List) akan butuh ruang tak terhingga untuk disimpan: sebuah List yang berisi List yang berisi List, terus tanpa henti. Box<List> adalah pointer berukuran tetap, jadi compiler bisa menghitung berapa banyak ruang yang dibutuhkan.

Aturan ownership bilang sebuah nilai punya tepat satu pemilik. Hampir selalu itu memang tepat, tapi kadang kamu benar-benar butuh beberapa bagian programmu berbagi data yang sama. Rc<T> (“reference counted”, dihitung referensinya) memungkinkan hal itu, dengan mencatat berapa banyak pemilik yang ada saat ini:

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("data bersama"));
let b = Rc::clone(&a); // bukan salinan penuh, hanya pemilik tambahan
let c = Rc::clone(&a);
println!("{a} {b} {c}");
println!("Jumlah pemilik: {}", Rc::strong_count(&a));
}

Output:

data bersama data bersama data bersama
Jumlah pemilik: 3
▶ Coba sendiri

Rc::clone tidak menyalin data di dalamnya. Ia hanya menaikkan sebuah penghitung dan memberikan referensi lain ke nilai yang sama. Datanya baru dibersihkan setelah semua pemiliknya keluar dari scope.

RefCell<T>: mengubah nilai lewat referensi bersama

Section titled “RefCell<T>: mengubah nilai lewat referensi bersama”

Biasanya, aturan borrowing diperiksa saat kompilasi: boleh banyak referensi hanya-baca, atau satu referensi mutable, tapi tidak boleh keduanya sekaligus. RefCell<T> memindahkan pemeriksaan yang sama itu ke saat program berjalan (runtime), sehingga kamu bisa mengubah nilai bahkan lewat sesuatu yang terlihat seperti referensi bersama tanpa mut:

use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());
}

Output:

6
▶ Coba sendiri

RefCell paling sering dipasangkan dengan Rc. Rc<RefCell<T>> memberimu nilai dengan banyak pemilik yang tetap bisa diubah, sesuatu yang tidak bisa dilakukan Rc<T> sendirian.

Jangan langsung mulai dengan smart pointer. Mulailah dengan ownership dan borrowing biasa, dan pakai smart pointer hanya saat compiler jelas-jelas bilang caramu tidak muat: tipe rekursif (Box), nilai yang memang butuh banyak pemilik (Rc), atau perubahan lewat referensi bersama yang tidak diizinkan aturan borrowing (RefCell).

Pakai enum List dari bagian Box. Buat list 1, 2, 3 dan tulis fungsi fn sum(list: &List) -> i32 yang menjumlahkan semua nilainya. Petunjuk: lakukan match pada list-nya. Untuk Cons(value, rest), jawabannya adalah value ditambah jumlah dari rest. Untuk Nil, jawabannya 0.

Lihat solusi
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, rest) => value + sum(rest),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("Jumlah: {}", sum(&list));
}

Output:

Jumlah: 6
▶ Coba sendiri

Fungsi ini memanggil dirinya sendiri untuk sisa list sampai mencapai Nil. Perhatikan bahwa kamu bisa memberikan rest (sebuah &Box<List>) di tempat yang mengharapkan &List. Rust otomatis “melihat ke dalam” Box untukmu.

Buat satu daftar belanja (sebuah Vec<String>) yang dimiliki oleh dua orang, alice dan bob, dan keduanya bisa menambahkan barang ke daftar itu. Masing-masing menambahkan satu barang. Lalu cetak daftarnya dan jumlah pemiliknya. Petunjuk: bungkus Vec-nya dengan Rc<RefCell<...>>, beri setiap orang sebuah Rc::clone, lalu panggil .borrow_mut().push(...) untuk menambah barang.

Lihat solusi
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let list = Rc::new(RefCell::new(Vec::new()));
let alice = Rc::clone(&list);
let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("susu"));
bob.borrow_mut().push(String::from("roti"));
println!("{:?}", list.borrow());
println!("Jumlah pemilik: {}", Rc::strong_count(&list));
}

Output:

["susu", "roti"]
Jumlah pemilik: 3
▶ Coba sendiri

Ada tiga pemilik: list, alice, dan bob. Ketiganya menunjuk ke Vec yang sama, jadi barang yang ditambahkan lewat alice juga terlihat lewat bob dan list. Tidak ada variabel yang butuh mut, karena RefCell yang mengurus perubahannya.

  • Box<T> menyimpan nilai di heap dengan pointer berukuran tetap. Ini dibutuhkan untuk tipe rekursif.
  • Rc<T> memungkinkan banyak pemilik untuk nilai yang sama di kode single-thread, lewat reference counting; pakai Arc<T> untuk antar-thread.
  • RefCell<T> memeriksa aturan borrowing saat program berjalan, bukan saat kompilasi, sehingga nilai bisa diubah lewat referensi bersama. Tapi program akan panic kalau aturannya dilanggar.
  • Pakai smart pointer hanya saat ownership dan borrowing biasa tidak cocok untuk situasimu.

Kuis singkat

1. Kenapa tipe rekursif seperti node linked list butuh Box<T>?

2. Apa yang sebenarnya dilakukan Rc::clone(&a)?

3. Kapan RefCell<T> memeriksa aturan borrowing?

4. Apa yang diberikan Rc<RefCell<T>> yang tidak bisa diberikan Rc<T> sendirian?

Skor: 0 / 4