Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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> dan RefCell<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 dalam RefCell<T> bahkan saat RefCell<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.

Filename: src/lib.rs
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!");
        }
    }
}
Listing 15-20: Sebuah library buat melacak seberapa dekat suatu nilai ke nilai maksimalnya dan ngasih peringatan saat nilai tersebut mencapai tingkat tertentu

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.

Filename: src/lib.rs
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);
    }
}
Listing 15-21: Sebuah usaha buat mengimplementasikan sebuah MockMessenger yang mana tidak diizinkan oleh borrow checker

Kode 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.

Filename: src/lib.rs
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);
    }
}
Listing 15-22: Memakai RefCell<T> buat memutasi nilai internal (inner value) sembari nilai luarnya (outer value) dianggap immutable

Field 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.

Filename: src/lib.rs
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);
    }
}
Listing 15-23: Membikin dua referensi mutable di dalam scope yang sama buat melihat kalau RefCell<T> bakal mengalami panic

Kita 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.

Filename: src/main.rs
#[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:?}");
}
Listing 15-24: Memakai Rc<RefCell<i32>> buat bikin sebuah List yang bisa kita mutasi

Kita 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.