Aturan borrowing yang kita pegang sejauh ini dijalankan oleh kompilator, sebelum program berjalan satu detik pun. Ketegasan itu menyelamatkan kita dari data race dan dangling reference, tetapi ada pola program yang sah secara logika namun kaku secara aturan: objek penghitung yang dibagikan sebagai pinjaman &, logger yang mencatat pesan lewat method &self, dan cache yang mengisi dirinya sendiri saat dibaca. Semuanya membutuhkan mutasi di balik reference yang tampak immutable — pola yang dikenal sebagai interior mutability. Rust menyediakan RefCell<T> untuk pola ini: smart pointer yang tetap menjalankan aturan borrowing secara penuh, tetapi memindahkan pemeriksaannya dari waktu kompilasi ke waktu berjalannya program. Konsekuensinya besar: kode yang melanggar aturan akan lolos dari kompilator, lalu meledak sebagai panic di runtime.

Masalah: Method yang Logisnya Mengubah Data #

Kita mulai dari kasus nyata yang tidak bisa dibantah. Sebuah penghitung kunjungan toko ingin dicatat oleh dua modul berbeda — modul pintu masuk dan modul promo — dan kedua modul memegangnya sebagai pinjaman &VisitCounter karena keduanya hanya “memakai” penghitung, bukan memilikinya. Setiap pencatatan kunjungan jelas adalah mutasi: angka total harus naik. Namun method pencatat hanya menerima &self — jika kita mendeklarasikan field total: u32 biasa, penulisan *self.total += 1 di dalam method &self langsung ditolak kompilator karena field yang diubah harus diakses lewat &mut.

Konflik ini tidak bisa diselesaikan dengan berganti tanda: menjadikan method pencatat sebagai &mut self memaksa kedua modul memegang &mut VisitCounter, dan aturan “satu mutable reference atau banyak immutable reference” melarang dua modul memegang pinjangan &mut tersebut bersamaan. Kita terjebak di antara API yang alami (&self) dan data yang memang berubah. Jalan keluarnya bukan melonggarkan aturan, melainkan memindahkan tempat aturan diperiksa.

Mekanisme RefCell: Aturan Borrow di Runtime #

RefCell<T> membungkus satu nilai T dan menempatkan pencatatan pinjaman di sampingnya: sebuah penanda status yang mencatat berapa banyak pinjaman shared dan apakah ada pinjaman mutable yang sedang aktif. Akses ke data hanya boleh lewat dua method penjaga: borrow() yang mengembalikan Ref<T>, dan borrow_mut() yang mengembalikan RefMut<T>. Kedua penjaga ini adalah pemegang pintu bergaya RAII — persis pola trait Drop dari Chapter sebelumnya: saat penjaga dijatuhkan (keluar scope), status pinjaman dikembalikan ke keadaan bersih.

Aturannya identik dengan aturan borrowing yang sudah kita hafal: banyak Ref boleh hidup bersamaan, tetapi RefMut harus menjadi satu-satunya penghuni. Bedanya cuma penjaganya: bukan kompilator yang memeriksa, melainkan RefCell sendiri pada saat program berjalan. Panggilan borrow_mut() ketika masih ada Ref hidup, atau borrow() ketika masih ada RefMut hidup, memicu panic yang menghentikan program. Konsekuensi praktisnya: kesalahan yang sebelumnya tertangkap saat kompilasi kini baru terlihat saat kode jalur yang bermasalah benar-benar dieksekusi.

Interior Mutability lewat &self #

Mari kita bangun penghitung kunjungan yang utuh:

rust
use std::cell::RefCell;

struct VisitCounter {
    total: RefCell<u32>,
}

impl VisitCounter {
    fn new() -> VisitCounter {
        VisitCounter { total: RefCell::new(0) }
    }

    fn record_visit(&self) {
        *self.total.borrow_mut() += 1;
    }

    fn current_total(&self) -> u32 {
        *self.total.borrow()
    }
}

fn main() {
    let counter = VisitCounter::new();
    let visitor_a = &counter;
    let visitor_b = &counter;

    visitor_a.record_visit();
    visitor_b.record_visit();
    visitor_a.record_visit();

    println!("total kunjungan: {}", counter.current_total());
}

Output yang Dihasilkan:

text
total kunjungan: 3

Mari kita bedah kode di atas:

  1. Baris 4 (total: RefCell<u32>): angka total dibungkus RefCell — dari luar struct, field ini tampak sebagai data biasa, dan mutabilitasnya disembunyikan di dalam.
  2. Baris 12 (fn record_visit(&self)): method pencatat hanya menerima &self, persis seperti yang dibutuhkan dua modul pemakai. Kata kunci mut tidak muncul di signature, dan variabel counter di main pun tidak dideklarasikan mut.
  3. Baris 13 (*self.total.borrow_mut() += 1;): urutan kerjanya berlapis: borrow_mut() meminta penjaga RefMut<u32> ke RefCell, operator * mengikuti penjaga menuju angka u32, += 1 menaikkan angkanya, lalu penjaga dijatuhkan begitu statement selesai sehingga status pinjaman bersih kembali. Peminjaman terjadi sesaat — hanya sepanjang satu statement.
  4. Baris 16 (fn current_total(&self)): pembacaan memakai penjaga Ref<u32> lewat borrow() — banyak pembaca diizinkan, sehingga method ini aman dipanggil siapa pun kapan pun.
  5. Baris 23-24 (visitor_a dan visitor_b): dua reference immutable atas counter hidup bersamaan, lalu tiga pemanggilan record_visit() pada Baris 26-28 berjalan melalui kedua pinjaman tersebut — di mata kompilator, kedua pemanggilan hanyalah pemakaian &self biasa. Seluruh mutasi terjadi di balik pintu RefCell, dan program menghasilkan angka 3 yang benar.

Banyak Pembaca Tetap Diperbolehkan #

Aturan runtime RefCell meniru aturan kompilator secara utuh, termasuk izin banyak pembaca. Dua Ref yang hidup bersamaan adalah skenario yang sah:

rust
use std::cell::RefCell;

fn main() {
    let notes = RefCell::new(String::from("catatan penting"));

    let reader_a = notes.borrow();
    let reader_b = notes.borrow();

    println!("pembaca A melihat: {}", reader_a);
    println!("pembaca B melihat: {}", reader_b);
}

Output yang Dihasilkan:

text
pembaca A melihat: catatan penting
pembaca B melihat: catatan penting

Mari kita bedah kode di atas:

  1. Baris 6-7 (notes.borrow() dua kali): penjaga Ref pertama belum dijatuhkan ketika penjaga kedua dibuat — pola “banyak pembaca” ini lolos karena tidak ada RefMut yang aktif.
  2. Baris 9-10 (println!("pembaca A melihat: {}", reader_a)): penjaga Ref<String> tampil di layar seolah-olah String biasa — Ref mengimplementasikan Deref dengan target T, sehingga makro println! membaca isi tanpa membongkar penjaga secara manual.
  3. Setelah Baris 10: kedua penjaga dijatuhkan di akhir scope main sesuai urutan terbalik, status pinjaman kembali kosong, dan RefCell siap dipinjam ulang.

Pelanggaran yang Baru Muncul di Runtime #

Sekarang kita uji sisi gelapnya. Pertama, penulisan yang mencoba masuk saat pembaca masih memegang data:

rust
use std::cell::RefCell;

fn main() {
    let notes = RefCell::new(String::from("catatan penting"));

    let reader = notes.borrow();
    let _editor = notes.borrow_mut();

    println!("{}", reader);
}

Program ini lolos kompilasi tanpa satu pun error, lalu meledak saat dijalankan:

text
thread 'main' (17404) panicked at main.rs:7:25:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Kebalikannya juga mematikan — pembaca yang mencoba masuk saat penulis masih bekerja:

rust
use std::cell::RefCell;

fn main() {
    let notes = RefCell::new(String::from("catatan penting"));

    let _editor = notes.borrow_mut();
    let reader = notes.borrow();

    println!("{}", reader);
}

Pesan panic saat program berjalan:

text
thread 'main' (17406) panicked at main.rs:7:24:
RefCell already mutably borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Mari kita bedah kedua kejadian tersebut:

  1. Program pertama, Baris 7 (let _editor = notes.borrow_mut();): penjaga reader dari Baris 6 masih hidup, sehingga RefCell menolak pinjaman mutable dengan panic RefCell already borrowed. Lokasi main.rs:7:25 menunjuk tepat ke pemanggilan borrow_mut() — kolom ke-25 adalah posisi nama method pada baris tersebut. Angka dalam kurung setelah main adalah penanda thread yang nilainya berubah pada setiap eksekusi.
  2. Program kedua, Baris 7 (let reader = notes.borrow();): kasus cerminnya — penjaga RefMut dari Baris 6 masih memegang pintu, dan panic RefCell already mutably borrowed mencegah pembaca masuk. Awalan _ pada _editor menjaga penjaga tetap hidup sampai akhir scope; tanpa pengikatan, RefMut akan dijatuhkan seketika dan program justru berjalan mulus.
  3. Kedua program lolos kompilasi: inilah pembeda paling penting dari semua error sebelumnya dalam Course ini. Kompilator hanya melihat dua pemanggilan method biasa pada &notes — pelanggaran aturan hanya bisa dideteksi oleh RefCell ketika kedua penjaga benar-benar bertabrakan di runtime.

Ringkasan #

RefCell<T> menghidupkan pola interior mutability: data berubah di balik reference &self sementara aturan borrowing tetap berlaku — hanya saja pemeriksaannya pindah ke runtime. Method borrow() dan borrow_mut() mengeluarkan penjaga Ref dan RefMut yang menjaga status pinjaman RAII, banyak pembaca boleh hadir bersamaan, dan pelanggaran eksklusivitas berakhir sebagai panic RefCell already borrowed atau RefCell already mutably borrowed — bukan pesan kompilator. Kini dua keping yang terpisah sudah lengkap: Rc menyediakan banyak pemilik read-only, RefCell menyediakan mutasi lewat reference immutable. Chapter berikutnya menyatukan keduanya menjadi pola paling terkenal di keluarga smart pointer: Rc<RefCell<T>>.