RefCell<T>

Interior mutability dan pemeriksaan aturan borrow saat runtime.

Daftar isi

Topik ini membahas RefCell<T>, yaitu smart pointer yang menghadirkan interior mutability, yakni kemampuan mengubah nilai dari balik pinjaman baca, dengan memindahkan pemeriksaan aturan pinjaman dari saat kompilasi ke saat program berjalan. Pembahasan mencakup makna interior mutability, cara kerja gerbang borrow dan borrow_mut, mutasi data dari dalam metode yang menerima &self, serta bentuk panic yang muncul ketika aturan satu penulis atau banyak pembaca dilanggar pada runtime. Setelah membaca topik ini, keputusan memakai RefCell dapat diambil dengan menimbang keringanan struktur terhadap risiko panic yang muncul menjauh dari penyebabnya.

Interior mutability: mutasi dari balik pinjaman baca#

Aturan pinjaman menyatakan bahwa nilai yang sedang dipinjam secara baca tidak boleh diubah, dan pemeriksaannya dilakukan borrow checker saat kompilasi berdasarkan alur kode yang dapat dilihat sepenuhnya. Sebagian pola berada di luar jangkauan pemeriksaan itu meskipun logikanya aman, misalnya sebuah metode yang menerima &self tetapi ingin menambah isi catatan internal, atau struct yang menghitung jumlah pemanggilan tanpa mengubah tanda tangannya. Kompilator tidak dapat membenarkan mutasi semacam itu karena aturan pinjaman diperiksa tanpa mengetahui keadaan program saat berjalan, dan penolakannya berupa error pinjaman yang sudah dibahas pada topik aturan borrowing.

RefCell<T> menjawab kebutuhan tersebut dengan menegakkan aturan yang sama pada waktu yang berbeda. Nilai yang dibungkus tetap dipegang satu pemilik, tetapi setiap akses harus melewati gerbang pinjaman yang keadaannya dicatat sebagai data saat program berjalan, bukan diperiksa saat kompilasi. Ketika aturan satu penulis atau banyak pembaca dilanggar pada saat program berjalan, program berhenti dengan panic alih-alih dibiarkan merusak memori. Jaminan keamanan tetap utuh, dan yang berubah hanyalah titik waktu penegakannya beserta bentuk kegagalannya.

Gerbang borrow dan borrow_mut#

Dua gerbang tersebut berbentuk metode. Metode borrow() mengembalikan Ref<T> yang bersifat pinjaman baca, dan metode borrow_mut() mengembalikan RefMut<T> yang bersifat pinjaman tulis. Kedua nilai penjaga ini dijatuhkan ketika keluar dari lingkupnya, dan pada saat itulah pinjaman pada RefCell dinyatakan selesai. Aturan yang sama dengan referensi biasa tetap berlaku: beberapa Ref boleh hidup bersamaan, satu RefMut harus sendirian, dan campuran keduanya dilarang.

rust sel-mutasi.rs
use std::cell::RefCell;

fn main() {
    let sel = RefCell::new(5);

    {
        let mut nilai = sel.borrow_mut();
        *nilai += 100;
    } // pinjaman tulis berakhir di sini

    println!("nilai sekarang: {}", sel.borrow());
}
text Keluaran
nilai sekarang: 105

Program tersebut mengubah nilai dari lima menjadi seratus lima meskipun variabel sel tidak dideklarasikan dengan mut, karena mutasi terjadi di balik gerbang borrow_mut. Blok kurung kurawal membatasi umur RefMut: penjaga pinjaman dijatuhkan pada kurung kurawal penutup, sehingga pinjaman tulis resmi berakhir sebelum pinjaman baca pada println! dibuat. Susunan seperti ini adalah pola dasar pemakaian RefCell, yaitu mengakhiri setiap pinjaman sebelum pinjaman berikutnya dimulai.

Mengubah data dari dalam metode &self#

Kegunaan yang paling umum dari interior mutability adalah metode yang tanda tangannya hanya menjanjikan pembacaan. Program berikut membangun Pencatat, yakni pencatat baris sensor, yang menyimpan isi catatannya di dalam RefCell<Vec<String>>. Metode catat menerima &self dan tetap dapat menambah baris, karena penambahan berjalan melalui borrow_mut pada medan internal.

rust catatan-sensor.rs
use std::cell::RefCell;

struct Pencatat {
    catatan: RefCell<Vec<String>>,
}

impl Pencatat {
    fn baru() -> Pencatat {
        Pencatat {
            catatan: RefCell::new(Vec::new()),
        }
    }

    fn catat(&self, teks: &str) {
        self.catatan.borrow_mut().push(String::from(teks));
    }

    fn banyak(&self) -> usize {
        self.catatan.borrow().len()
    }
}

fn main() {
    let pencatat = Pencatat::baru();
    let rujukan = &pencatat; // pinjaman baca biasa

    rujukan.catat("suhu 30");
    rujukan.catat("suhu 31");
    rujukan.catat("suhu 30");

    println!("banyak catatan: {}", rujukan.banyak());
    println!("isi terakhir  : {}", pencatat.catatan.borrow().last().map(|s| s.as_str()).unwrap_or("-"));
}
text Keluaran
banyak catatan: 3
isi terakhir  : suhu 30

Keluaran tersebut membuktikan tiga baris masuk ke dalam catatan melalui rujukan yang merupakan pinjaman baca biasa atas pencatat. Tanda tangan catat(&self, ...) menjanjikan bahwa pemanggil tidak kehilangan kendali apa pun, dan pemanggilan dapat berlangsung berulang kali atas variabel yang sama tanpa deklarasi mut. Pada baris terakhir, pinjaman baca atas catatan dibuat hanya sepanjang satu ekspresi sehingga selesai tepat setelah nilai terakhirnya diambil. Pola seperti ini dipakai untuk penghitung, cache internal, dan pencatat kejadian yang tidak boleh mengubah antarmuka publik suatu tipe.

Pelanggaran aturan memicu panic saat runtime#

Pinjaman pada RefCell dicatat sebagai data sehingga kompilator tidak melihatnya sama sekali. Akibatnya, pelanggaran aturan baru terdeteksi saat program berjalan. Program berikut membuat pinjaman baca baca yang masih dipakai di baris berikutnya, lalu membuat pinjaman tulis tulis pada saat pinjaman baca itu masih hidup, sehingga borrow_mut menghentikan program.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# buat program yang meminjam baca dan tulis pada RefCell secara bersamaan
printf 'use std::cell::RefCell;\n\nfn main() {\n    let sel = RefCell::new(vec![10, 24, 100]);\n\n    let baca = sel.borrow();\n    let tulis = sel.borrow_mut(); // ditolak saat runtime\n\n    println!("{} elemen terbaca", baca.len());\n    println!("elemen pertama {}", tulis[0]);\n}\n' > pinjam-ganda.rs
# kompilasi lalu jalankan, nomor thread pada pesan bervariasi sehingga disaring
rustc --edition=2021 pinjam-ganda.rs -o pinjam-ganda && ./pinjam-ganda 2>&1 | sed -E 's/ \([0-9]+\)//' | sed -n '1,5p'
text Keluaran
thread 'main' panicked at pinjam-ganda.rs:7:21:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Pesan RefCell already borrowed menyatakan bahwa gerbang pinjaman menolak permintaan baru karena pinjaman lama belum selesai, dan lokasi pinjam-ganda.rs:7:21 menunjuk pemanggilan borrow_mut yang menjadi titik penolakan. Program kemudian berhenti dengan kode keluar 101 seperti kegagalan panic lainnya. Pada referensi biasa, susunan dua baris yang sama ditolak jauh sebelum program berjalan dengan error E0502, karena titik pembuatan dan pemakaian terakhir seluruh referensi dapat dilihat kompilator. Diagram berikut merangkum perbedaan posisi pemeriksaan pada kedua mekanisme itu.

aturan pinjaman tetap: satu penulis atau banyak pembaca&T dan &mut Tdiperiksa saat kompilasierror E0502 / E0499borrow() / borrow_mut()diperiksa saat runtimepanic saat pelanggaran
Aturan pinjaman yang sama, diperiksa pada waktu yang berbeda oleh referensi dan RefCell

Kesalahan umum#

Kesalahan pertama adalah menahan penjaga pinjaman terlalu lama, misalnya menyimpan hasil borrow() ke variabel yang hidup sepanjang fungsi sementara bagian lain fungsi itu memanggil borrow_mut. Penolakannya tidak muncul saat kompilasi seperti pada referensi, melainkan muncul sebagai panic pada baris pemanggilan borrow_mut, jauh dari deklarasi pinjaman bacanya. Perbaikannya adalah memperkecil lingkup setiap pinjaman: bungkus dengan blok tersendiri, atau bentuk satu ekspresi seperti sel.borrow_mut().push(nilai) yang menutup pinjaman pada akhir ekspresi.

Kesalahan kedua adalah memakai RefCell sebagai jalan pintas untuk melewati borrow checker padahal kode masih dapat disusun ulang. Bila konflik terjadi karena pembacaan dan penulisan dicampur dalam satu alur, pemisahan menjadi fase baca lalu fase tulis biasanya menyelesaikannya tanpa biaya runtime. RefCell tepat dipakai ketika mutasi memang harus terjadi dari balik antarmuka baca, misalnya penghitung internal atau catatan kejadian, bukan ketika penataan ulang kode terasa merepotkan.

Ringkasan#

  • RefCell<T> menghadirkan interior mutability: nilai diubah melalui gerbang borrow() dan borrow_mut() meskipun pemiliknya dipegang lewat pinjaman baca.
  • Aturan satu penulis atau banyak pembaca tetap berlaku, tetapi diperiksa saat runtime sehingga pelanggarannya berupa panic RefCell already borrowed dengan lokasi pemanggilan gerbang.
  • Metode dengan &self dapat mengubah medan internal yang dibungkus RefCell, sebagaimana pencatat baris sensor pada contoh.
  • Setiap pinjaman dijaga agar sependek mungkin, dan RefCell dipilih hanya bila mutasi dari balik antarmuka baca memang diperlukan.

Referensi#