RefCell<T> dan Desain Pola Interior Mutability
Interior mutability (mutabilitas interior) adalah sebuah desain pola di Rust
yang membiarkan kita memutasi (mengubah) data bahkan saat ada referensi
immutable ke data tersebut; normalnya, tindakan ini tidak diizinkan oleh
aturan borrowing. Buat memutasi data, pola ini memakai kode unsafe di dalam
sebuah struktur data untuk membengkokkan aturan-aturan normal Rust yang mengatur
mutasi dan borrowing. Kode unsafe mengindikasikan ke compiler bahwa kita
memeriksa aturan-aturan tersebut secara manual ketimbang mengandalkan
compiler buat mengeceknya untuk kita; kita bakal membahas kode unsafe
lebih banyak di Bab 20.
Kita bisa memakai tipe yang menggunakan desain pola interior mutability cuma
pas kita bisa memastikan kalau aturan borrowing bakal dipatuhi saat runtime,
meskipun compiler tidak bisa menjamin hal itu. Kode unsafe yang terlibat
kemudian dibungkus di dalam sebuah API yang aman (safe API), dan tipe yang ada
di luar bakal tetap immutable.
Mari kita eksplorasi konsep ini dengan melihat tipe RefCell<T> yang mengikuti
desain pola interior mutability ini.
Menegakkan Aturan Borrowing saat Runtime dengan RefCell<T>
Tidak seperti Rc<T>, tipe RefCell<T> merepresentasikan kepemilikan tunggal
(single ownership) atas data yang dipegangnya. Jadi apa yang membikin RefCell<T>
berbeda dari tipe seperti Box<T>? Ingat kembali aturan borrowing yang
kita pelajari di Bab 4:
- Pada waktu kapan pun, kita cuma boleh punya satu referensi mutable atau sejumlah referensi immutable (tapi tidak boleh dua-duanya sekaligus).
- Referensi harus selalu valid.
Dengan referensi biasa dan Box<T>, invarian (aturan mutlak) dari aturan
borrowing ini ditegakkan (enforced) saat compile time. Dengan RefCell<T>,
invarian ini ditegakkan saat runtime. Dengan referensi, kalau kita
melanggar aturan-aturan ini, kita bakal dapat error compiler. Dengan
RefCell<T>, kalau kita melanggar aturan-aturan ini, program kita bakal
mengalami panic dan keluar (exit).
Keuntungan dari mengecek aturan borrowing saat compile time adalah error bakal ditangkap lebih awal di proses development (pengembangan), dan tidak ada dampak pada performa runtime karena semua analisisnya udah diselesaikan sebelumnya. Karena alasan-alasan tersebut, mengecek aturan borrowing saat compile time adalah pilihan terbaik buat mayoritas kasus, yang mana itulah alasannya kenapa ini jadi default (bawaan) di Rust.
Keuntungan dari mengecek aturan borrowing saat runtime sebagai gantinya adalah ada beberapa skenario aman-memori (memory-safe) yang kemudian jadi diizinkan, di mana mereka tadinya tidak bakal diizinkan sama pengecekan compile time. Analisis statis, seperti compiler Rust, secara bawaan sifatnya konservatif. Beberapa properti (sifat) kode mustahil buat dideteksi dengan hanya menganalisis kodenya saja: contoh paling terkenal adalah Halting Problem, yang ada di luar cakupan buku ini tapi merupakan topik yang menarik buat diteliti.
Karena beberapa analisis itu mustahil, kalau compiler Rust tidak bisa yakin
kodenya mematuhi aturan ownership, ia mungkin bakal menolak sebuah program
yang sebenarnya benar; di sinilah letak sifat konservatifnya. Kalau Rust
menerima program yang salah, para user tidak bakal bisa mempercayai jaminan
yang diberikan Rust. Namun, kalau Rust menolak program yang benar, si
programmer bakal direpotkan, tapi tidak ada bencana besar yang bakal
terjadi. Tipe RefCell<T> ini berguna saat kita yakin kode kita mematuhi
aturan borrowing tapi compiler tidak mampu memahami dan menjamin hal
tersebut.
Sama halnya dengan Rc<T>, RefCell<T> cuma buat dipakai di skenario
single-threaded dan bakal ngasih kita error compile-time kalau kita
mencoba memakainya di konteks multithreaded. Kita bakal bahas gimana
cara mendapatkan fungsionalitas dari RefCell<T> di dalam program
multithreaded di Bab 16.
Berikut adalah rangkuman (recap) dari alasan memilih Box<T>, Rc<T>,
atau RefCell<T>:
Rc<T>memungkinkan banyak pemilik (multiple owners) dari data yang sama;Box<T>danRefCell<T>cuma punya pemilik tunggal (single owners).Box<T>mengizinkan borrows (peminjaman) immutable atau mutable yang dicek saat compile time;Rc<T>cuma mengizinkan borrows immutable yang dicek saat compile time;RefCell<T>mengizinkan borrows immutable atau mutable yang dicek saat runtime.- Karena
RefCell<T>mengizinkan borrows mutable yang dicek saat runtime, kita bisa memutasi nilai di dalamRefCell<T>bahkan saatRefCell<T>itu sendiri sifatnya immutable.
Memutasi nilai yang ada di dalam sebuah nilai yang immutable itulah yang disebut desain pola interior mutability. Mari kita lihat sebuah situasi di mana interior mutability itu berguna dan memeriksa gimana hal itu bisa dilakukan.
Interior Mutability: Peminjaman Mutable ke sebuah Nilai yang Immutable
Sebuah konsekuensi dari aturan borrowing adalah pas kita punya nilai yang immutable, kita tidak bisa meminjamnya secara mutable. Misalnya, kode ini tidak bakal bisa di-compile:
fn main() {
let x = 5;
let y = &mut x;
}
Kalau kita mencoba men-compile kode ini, kita bakal dapat error berikut:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Namun, ada beberapa situasi di mana bakal berguna buat sebuah nilai buat
bisa memutasi dirinya sendiri di dalam method-methodnya tapi terlihat
immutable bagi kode lain. Kode di luar method dari nilai tersebut tidak
bakal bisa memutasi nilai itu. Memakai RefCell<T> adalah salah satu
cara buat bisa mendapatkan kemampuan buat punya interior mutability, tapi
RefCell<T> tidak sepenuhnya menghindari aturan borrowing: borrow checker
di dalam compiler memang mengizinkan interior mutability ini, dan sebagai
gantinya aturan borrowing dicek saat runtime. Kalau kita melanggar
aturan-aturannya, kita bakal dapat panic! bukannya error compiler.
Mari kita kerjakan sebuah contoh praktis di mana kita bisa memakai
RefCell<T> buat memutasi nilai yang immutable dan melihat kenapa hal
itu berguna.
Use Case untuk Interior Mutability: Mock Objects
Kadang-kadang saat lagi testing (pengujian), seorang programmer bakal memakai sebuah tipe sebagai pengganti dari tipe lain, untuk mengobservasi perilaku tertentu dan menegaskan kalau perilakunya diimplementasikan dengan benar. Tipe placeholder ini disebut test double (pengganti tes). Bayangkan saja seperti pemeran pengganti (stunt double) di pembuatan film, di mana seseorang masuk dan menggantikan sang aktor buat melakukan adegan yang sangat berbahaya (tricky scene). Test doubles menggantikan tipe lain saat kita sedang menjalankan pengujian. Mock objects (objek pura-pura) adalah jenis test double spesifik yang merekam (records) apa yang terjadi selama pengujian berjalan sehingga kita bisa menegaskan kalau aksi yang benar telah terjadi.
Rust tidak punya objects (objek) dalam artian yang sama kayak bahasa lain punya objects, dan Rust tidak punya fungsionalitas mock object bawaan di standard library seperti yang ada di beberapa bahasa lain. Namun, kita tetap bisa membikin sebuah struct yang bakal melayani tujuan yang sama dengan sebuah mock object.
Ini skenario yang bakal kita uji: kita bakal bikin sebuah library yang melacak (tracks) sebuah nilai berdasarkan nilai maksimalnya dan mengirim pesan berdasarkan seberapa dekat nilai saat ini ke nilai maksimalnya. Library ini bisa dipakai buat melacak kuota user untuk jumlah pemanggilan API yang diizinkan buat mereka, contohnya.
Library kita cuma bakal menyediakan fungsionalitas buat melacak seberapa
dekat suatu nilai ke nilai maksimalnya dan apa pesan yang seharusnya muncul
di waktu kapan aja. Aplikasi-aplikasi yang memakai library kita bakal
diharapkan buat menyediakan mekanisme pengiriman pesannya: aplikasi itu bisa
aja menaruh pesannya di dalam aplikasinya, mengirim email, mengirim pesan
teks (SMS), atau melakukan hal lain. Library-nya tidak perlu tahu detail
tersebut. Yang ia butuhkan cuma sesuatu yang mengimplementasikan sebuah trait
yang bakal kita sediakan bernama Messenger. Listing 15-20 menunjukkan kode
dari library tersebut.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Satu bagian penting dari kode ini adalah trait Messenger punya satu method
bernama send yang menerima sebuah referensi immutable ke self dan teks
pesannya. Trait ini adalah interface (antarmuka) yang perlu
diimplementasikan oleh mock object kita sehingga si mock (objek
pura-pura) ini bisa dipakai dengan cara yang sama kayak objek aslinya dipakai.
Bagian penting lainnya adalah kita mau menguji perilaku dari method
set_value di LimitTracker. Kita bisa mengubah nilai apa yang kita teruskan
ke dalam parameter value, tapi set_value tidak mengembalikan apa pun yang
bisa kita pakai buat membuat penegasan (assertions). Kita mau bisa bilang kalau
saat kita bikin sebuah LimitTracker dengan sesuatu yang mengimplementasikan
trait Messenger dan sebuah nilai spesifik buat max, pas kita meneruskan
angka-angka yang beda buat value maka si messenger (pengirim pesan) ini bakal
disuruh mengirim pesan-pesan yang sesuai.
Kita butuh sebuah mock object yang, alih-alih mengirim email atau SMS
saat kita memanggil send, dia cuma bakal mencatat pesan-pesan apa aja yang
disuruh untuk dikirim. Kita bisa membikin sebuah instance baru dari mock object
ini, membikin sebuah LimitTracker yang memakai mock object itu,
memanggil method set_value di LimitTracker, dan kemudian mengecek kalau
mock object itu memang punya pesan-pesan yang kita harapkan. Listing 15-21
menunjukkan sebuah usaha buat mengimplementasikan sebuah mock object buat
melakukan hal itu persis, tapi borrow checker tidak bakal mengizinkannya.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
MockMessenger yang mana tidak diizinkan oleh borrow checkerKode pengujian ini mendefinisikan sebuah struct MockMessenger yang punya field
sent_messages berisi Vec dari nilai-nilai String buat melacak
pesan-pesan apa aja yang dia disuruh buat kirim. Kita juga mendefinisikan
fungsi associated (terkait) new buat bikin nyaman pas membikin nilai
MockMessenger baru yang dimulai dengan list pesan yang kosong. Kita
kemudian mengimplementasikan trait Messenger untuk MockMessenger supaya
kita bisa ngasih sebuah MockMessenger ke sebuah LimitTracker. Di dalam
definisi dari method send, kita mengambil pesan yang diberikan sebagai
parameter lalu menyimpannya di list sent_messages milik MockMessenger.
Di dalam pengujiannya, kita menguji apa yang terjadi saat LimitTracker
disuruh buat nge-set value ke sesuatu yang jumlahnya lebih dari 75 persen
dari nilai max. Pertama kita bikin sebuah MockMessenger baru, yang mana
bakal dimulai dengan list pesan yang kosong. Terus kita bikin sebuah
LimitTracker baru dan ngasih dia sebuah referensi ke MockMessenger yang
baru itu dan nilai max sebesar 100. Kita memanggil method set_value di
LimitTracker dengan nilai 80, yang mana itu lebih dari 75 persen dari 100.
Lalu kita menegaskan kalau list pesan yang dilacak sama MockMessenger itu
seharusnya sekarang punya satu pesan di dalamnya.
Namun, ada satu masalah dengan pengujian ini, seperti yang ditunjukkan di sini:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Kita tidak bisa memodifikasi MockMessenger buat melacak pesan-pesannya
karena method send mengambil sebuah referensi immutable ke self.
Kita juga tidak bisa mengikuti saran dari teks error-nya buat memakai
&mut self di method impl dan juga di definisi trait-nya. Kita tidak
mau mengubah trait Messenger cuma demi bisa melakukan pengujian.
Sebaliknya, kita harus cari cara buat bikin kode pengujian kita bekerja
dengan benar dengan desain kita yang udah ada.
Ini adalah sebuah situasi di mana interior mutability bisa membantu!
Kita bakal menyimpan sent_messages di dalam sebuah RefCell<T>, dan kemudian
method send bakal bisa memodifikasi sent_messages buat menyimpan
pesan-pesan yang udah kita lihat. Listing 15-22 menunjukkan seperti apa rupa
dari perubahan tersebut.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> buat memutasi nilai internal (inner value) sembari nilai luarnya (outer value) dianggap immutableField sent_messages sekarang bertipe RefCell<Vec<String>> bukannya
Vec<String>. Di dalam fungsi new, kita membikin sebuah instance
RefCell<Vec<String>> baru di sekeliling vector kosong.
Untuk implementasi method send-nya, parameter pertamanya tetap berupa
pinjaman (borrow) immutable dari self, yang mana cocok sama definisi
trait-nya. Kita memanggil borrow_mut pada RefCell<Vec<String>> di dalam
self.sent_messages buat dapet sebuah referensi mutable ke nilai yang
ada di dalam RefCell<Vec<String>>, yaitu vector-nya. Terus kita bisa
manggil push pada referensi mutable ke vector tersebut buat melacak
pesan-pesan yang dikirim selama pengujiannya.
Perubahan terakhir yang harus kita buat adalah di penegasannya (assertion):
buat melihat ada berapa banyak item yang ada di vector internalnya, kita
memanggil borrow pada RefCell<Vec<String>> buat dapet sebuah referensi
immutable ke vector-nya.
Sekarang karena kita sudah melihat gimana cara memakai RefCell<T>, mari kita
gali lebih dalam soal gimana cara kerjanya!
Melacak Borrows saat Runtime dengan RefCell<T>
Saat membikin referensi immutable dan mutable, kita masing-masing
memakai sintaks & dan &mut. Dengan RefCell<T>, kita memakai method
borrow dan borrow_mut, yang merupakan bagian dari API aman yang dimiliki
sama RefCell<T>. Method borrow mengembalikan tipe smart pointer
Ref<T>, dan borrow_mut mengembalikan tipe smart pointer RefMut<T>.
Kedua tipe ini mengimplementasikan Deref, jadi kita bisa memperlakukan
mereka kayak referensi biasa.
RefCell<T> melacak berapa banyak smart pointers Ref<T> dan
RefMut<T> yang saat ini lagi aktif. Setiap kali kita memanggil borrow,
RefCell<T> menaikkan hitungan (count) jumlah borrows immutable yang
lagi aktif. Saat sebuah nilai Ref<T> keluar dari scope, hitungan dari
borrows immutable itu turun sebanyak 1. Persis sama kayak aturan
borrowing di compile-time, RefCell<T> membiarkan kita punya banyak
borrows immutable atau satu borrow mutable pada suatu waktu
kapan pun.
Kalau kita mencoba melanggar aturan ini, bukannya dapat error compiler
kayak yang bakal kita dapat pas pakai referensi, implementasi dari
RefCell<T> malah bakal panic di saat runtime. Listing 15-23 menunjukkan
sebuah modifikasi dari implementasi send di Listing 15-22. Kita sengaja
mencoba membikin dua borrows mutable aktif di scope yang sama buat
mengilustrasikan kalau RefCell<T> mencegah kita dari melakukan hal ini saat
runtime.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> bakal mengalami panicKita membikin sebuah variabel one_borrow buat smart pointer RefMut<T>
yang dikembalikan dari borrow_mut. Terus kita membikin borrow mutable
lainnya dengan cara yang sama di dalam variabel two_borrow. Ini membikin dua
referensi mutable di dalam scope yang sama, yang mana tidak diizinkan.
Saat kita menjalankan pengujian buat library kita, kode di Listing 15-23
bakal berhasil di-compile tanpa error apa pun, tapi pengujiannya bakal gagal:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Perhatikan kalau kodenya mengalami panic dengan pesan already borrowed: BorrowMutError. Ini adalah gimana RefCell<T> menangani pelanggaran dari
aturan borrowing saat runtime.
Memilih buat menangkap error borrowing saat runtime ketimbang saat compile
time, seperti yang udah kita lakuin di sini, berarti kita punya kemungkinan
buat nemuin kesalahan di kode kita jauh belakangan di proses pengembangan:
bahkan mungkin tidak bakal ketemu sampai kode kita sudah di-deploy ke
production (produksi). Selain itu, kode kita bakal kena sedikit penalti
(hukuman) performa runtime akibat harus melacak borrows-nya pas
runtime ketimbang pas compile time. Namun, memakai RefCell<T> memungkinkan
kita buat menulis sebuah mock object yang bisa memodifikasi dirinya sendiri buat
melacak pesan-pesan yang udah dilihatnya sementara kita memakainya di dalam sebuah
konteks di mana cuma nilai immutable yang diizinkan. Kita bisa memakai
RefCell<T> meskipun ada trade-offs (kekurangannya) buat dapat fungsionalitas
lebih banyak ketimbang yang disediakan oleh referensi biasa.
Mengizinkan Kepemilikan Ganda (Multiple Owners) pada Data yang Mutable dengan Rc<T> dan RefCell<T>
Cara yang umum buat memakai RefCell<T> adalah dengan menggabungkannya bersama
Rc<T>. Ingat kembali kalau Rc<T> membiarkan kita punya banyak pemilik
dari suatu data, tapi ia cuma ngasih akses immutable ke data tersebut. Kalau
kita punya sebuah Rc<T> yang memegang sebuah RefCell<T>, kita bisa
dapat sebuah nilai yang bisa punya banyak pemilik dan juga bisa kita mutasi
(ubah)!
Sebagai contoh, ingat kembali contoh cons list di Listing 15-18 di mana kita
memakai Rc<T> agar beberapa lists bisa berbagi kepemilikan dari sebuah list
lain. Karena Rc<T> cuma menampung nilai immutable, kita tidak bisa mengubah
satu pun nilai di dalam list-nya setelah kita membuatnya. Mari kita
menambahkan RefCell<T> agar kita bisa mendapatkan kemampuan buat mengubah
nilai-nilai di dalam lists tersebut. Listing 15-24 menunjukkan kalau dengan
memakai sebuah RefCell<T> di dalam definisi Cons, kita bisa memodifikasi
nilai yang disimpan di semua lists.
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
Rc<RefCell<i32>> buat bikin sebuah List yang bisa kita mutasiKita membikin sebuah nilai yang merupakan sebuah instance dari
Rc<RefCell<i32>> lalu menyimpannya ke dalam sebuah variabel bernama value
supaya kita bisa mengaksesnya secara langsung nanti. Terus kita membikin
sebuah List di dalam a dengan sebuah varian Cons yang memegang value.
Kita perlu meng-clone value sehingga baik a dan value dua-duanya punya
kepemilikan dari nilai internal 5 tersebut ketimbang memindahkan (transferring)
kepemilikan dari value ke a atau bikin a meminjam dari value.
Kita membungkus list a di dalam sebuah Rc<T> sehingga saat kita membikin
lists b dan c, mereka berdua bisa merujuk ke a, persis kayak yang kita
lakuin di Listing 15-18.
Setelah kita membikin lists di a, b, dan c, kita mau menambahkan 10 ke
nilai di dalam value. Kita melakukan ini dengan memanggil borrow_mut pada
value, yang mana memakai fitur automatic dereferencing yang udah kita bahas
di “Ke Mana Perginya Operator ->?” di Bab 5 buat
men-dereferensi Rc<T> ke nilai RefCell<T> di dalamnya. Method borrow_mut
mengembalikan sebuah smart pointer RefMut<T>, lalu kita memakai operator
dereferensi padanya dan mengubah nilai internalnya.
Saat kita mencetak a, b, dan c, kita bisa melihat kalau mereka semua
sekarang punya nilai yang udah dimodifikasi menjadi 15 bukannya 5:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Teknik ini lumayan keren! Dengan memakai RefCell<T>, dari luar kita punya
sebuah nilai List yang terlihat immutable. Tapi kita bisa memakai
method-method di RefCell<T> yang menyediakan akses ke interior mutability-nya
sehingga kita bisa memodifikasi data kita saat kita butuhkan. Pengecekan aturan
borrowing saat runtime ngelindungin kita dari data races (balapan data),
dan kadang-kadang sepadan rasanya buat menukar sedikit kecepatan (speed) demi
fleksibilitas di dalam struktur data kita ini. Perhatikan bahwa RefCell<T>
tidak bakal bekerja buat kode yang multithreaded! Mutex<T> adalah versi
yang thread-safe (aman dari utas) dari RefCell<T>, dan kita bakal membahas
Mutex<T> di Bab 16.