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:
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:
total kunjungan: 3Mari kita bedah kode di atas:
- Baris 4 (
total: RefCell<u32>): angka total dibungkusRefCell— dari luar struct, field ini tampak sebagai data biasa, dan mutabilitasnya disembunyikan di dalam. - Baris 12 (
fn record_visit(&self)): method pencatat hanya menerima&self, persis seperti yang dibutuhkan dua modul pemakai. Kata kuncimuttidak muncul di signature, dan variabelcounterdimainpun tidak dideklarasikanmut. - Baris 13 (
*self.total.borrow_mut() += 1;): urutan kerjanya berlapis:borrow_mut()meminta penjagaRefMut<u32>keRefCell, operator*mengikuti penjaga menuju angkau32,+= 1menaikkan angkanya, lalu penjaga dijatuhkan begitu statement selesai sehingga status pinjaman bersih kembali. Peminjaman terjadi sesaat — hanya sepanjang satu statement. - Baris 16 (
fn current_total(&self)): pembacaan memakai penjagaRef<u32>lewatborrow()— banyak pembaca diizinkan, sehingga method ini aman dipanggil siapa pun kapan pun. - Baris 23-24 (
visitor_adanvisitor_b): dua reference immutable atascounterhidup bersamaan, lalu tiga pemanggilanrecord_visit()pada Baris 26-28 berjalan melalui kedua pinjaman tersebut — di mata kompilator, kedua pemanggilan hanyalah pemakaian&selfbiasa. Seluruh mutasi terjadi di balik pintuRefCell, 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:
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:
pembaca A melihat: catatan penting
pembaca B melihat: catatan pentingMari kita bedah kode di atas:
- Baris 6-7 (
notes.borrow()dua kali): penjagaRefpertama belum dijatuhkan ketika penjaga kedua dibuat — pola “banyak pembaca” ini lolos karena tidak adaRefMutyang aktif. - Baris 9-10 (
println!("pembaca A melihat: {}", reader_a)): penjagaRef<String>tampil di layar seolah-olahStringbiasa —RefmengimplementasikanDerefdengan targetT, sehingga makroprintln!membaca isi tanpa membongkar penjaga secara manual. - Setelah Baris 10: kedua penjaga dijatuhkan di akhir scope
mainsesuai urutan terbalik, status pinjaman kembali kosong, danRefCellsiap dipinjam ulang.
Pelanggaran yang Baru Muncul di Runtime #
Sekarang kita uji sisi gelapnya. Pertama, penulisan yang mencoba masuk saat pembaca masih memegang data:
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:
thread 'main' (17404) panicked at main.rs:7:25:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceKebalikannya juga mematikan — pembaca yang mencoba masuk saat penulis masih bekerja:
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:
thread 'main' (17406) panicked at main.rs:7:24:
RefCell already mutably borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceMari kita bedah kedua kejadian tersebut:
- Program pertama, Baris 7 (
let _editor = notes.borrow_mut();): penjagareaderdari Baris 6 masih hidup, sehinggaRefCellmenolak pinjaman mutable dengan panicRefCell already borrowed. Lokasimain.rs:7:25menunjuk tepat ke pemanggilanborrow_mut()— kolom ke-25 adalah posisi nama method pada baris tersebut. Angka dalam kurung setelahmainadalah penanda thread yang nilainya berubah pada setiap eksekusi. - Program kedua, Baris 7 (
let reader = notes.borrow();): kasus cerminnya — penjagaRefMutdari Baris 6 masih memegang pintu, dan panicRefCell already mutably borrowedmencegah pembaca masuk. Awalan_pada_editormenjaga penjaga tetap hidup sampai akhir scope; tanpa pengikatan,RefMutakan dijatuhkan seketika dan program justru berjalan mulus. - Kedua program lolos kompilasi: inilah pembeda paling penting dari semua error sebelumnya dalam Course ini. Kompilator hanya melihat dua pemanggilan method biasa pada
¬es— pelanggaran aturan hanya bisa dideteksi olehRefCellketika 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>>.