Dua Chapter sebelumnya masing-masing menuntaskan separuh teka-teki: Rc memungkinkan banyak pemilik, tetapi hanya untuk data read-only; RefCell memungkinkan mutasi lewat reference immutable, tetapi tetap hanya punya satu pemilik. Kebutuhan dunia nyata kerap menuntut keduanya sekaligus — sebuah logger yang dipakai beramai-ramai oleh banyak modul, satu papan skor yang diperbarui banyak komponen, atau peta hubungan antar simpul yang tumbuh saat program berjalan. Kombinasi keduanya menghasilkan pola paling terkenal di keluarga smart pointer: Rc<RefCell<T>> — struktur data bersama yang bisa diubah. Pola ini juga menjadi ajang ujian sesungguhnya bagi seluruh pengetahuan kita: reference count, penjaga pinjaman runtime, deref berantai, dan urutan Drop semuanya bekerja serentak di dalam satu program.

Masalah: Logger Bersama yang Harus Bisa Ditulis #

Kita mulai dari skenario toko daring yang kongkret. Layanan pembayaran dan layanan pengiriman sama-sama perlu mencatat kejadian ke satu log yang sama — pembayaran masuk tercatat di sana, paket dikirim juga tercatat di sana, dan tim operasional membaca log itu sebagai satu kesatuan. Dua layanan ini hidup mandiri: keduanya dibangun di tempat berbeda, bisa saja dibongkar satu per satu saat program berjalan, dan tidak ada satu “pemilik utama” yang wajar untuk datanya. Kepemilikan bersama Rc menjawab kebutuhan pemiliknya, tetapi sebuah log yang tidak pernah bisa ditulis jelas tidak berguna — mutasi adalah inti keberadaannya. Rc<RefCell<...>> menyatukan dua kebutuhan yang bertentangan itu dalam satu tipe.

Mekanisme Rc<RefCell> #

Struktur bersarang ini bekerja sebagai dua lapis mekanisme yang saling melengkapi. Lapis luar, Rc, mengurus kepemilikan: petak heap menyimpan RefCell<Vec<String>> beserta reference count, setiap layanan memegang Rc::clone murah, dan logger hanya mati ketika pemilik terakhir pergi. Lapis dalam, RefCell, mengurus akses: setiap penulisan lewat borrow_mut() diperiksa di runtime, penjaga RefMut hidup hanya sepanjang satu operasi push, lalu status pinjaman bersih kembali.

Tipe pembaca Rc<RefCell<T>> bekerja mulus berkat deref berantai yang kita pelajari di Chapter Deref: ekspresi self.logger mengikuti Deref milik Rc menuju RefCell, sehingga method write milik SharedLogger bisa dipanggil langsung dari balik Rc tanpa pembongkaran. Kunci disiplinnya satu: penjaga pinjaman hidup sesingkat mungkin — dibuka di awal statement, ditutup di akhir statement yang sama — sehingga dua layanan yang menulis berurutan tidak pernah bertabrakan.

Dua Layanan, Satu Logger #

Mari kita rakit program lengkapnya:

rust
use std::cell::RefCell;
use std::rc::Rc;

struct SharedLogger {
    entries: RefCell<Vec<String>>,
}

impl SharedLogger {
    fn new() -> SharedLogger {
        SharedLogger { entries: RefCell::new(Vec::new()) }
    }

    fn write(&self, message: &str) {
        self.entries.borrow_mut().push(String::from(message));
    }

    fn total_entries(&self) -> usize {
        self.entries.borrow().len()
    }
}

struct PaymentService {
    logger: Rc<SharedLogger>,
}

struct ShippingService {
    logger: Rc<SharedLogger>,
}

impl PaymentService {
    fn new(logger: Rc<SharedLogger>) -> PaymentService {
        PaymentService { logger }
    }

    fn receive_payment(&self) {
        self.logger.write("pembayaran diterima");
    }
}

impl ShippingService {
    fn new(logger: Rc<SharedLogger>) -> ShippingService {
        ShippingService { logger }
    }

    fn dispatch_package(&self) {
        self.logger.write("paket dikirim ke gudang");
    }
}

fn main() {
    let logger = Rc::new(SharedLogger::new());

    let payment = PaymentService::new(Rc::clone(&logger));
    let shipping = ShippingService::new(Rc::clone(&logger));

    println!("strong count setelah dua layanan: {}", Rc::strong_count(&logger));

    payment.receive_payment();
    payment.receive_payment();
    shipping.dispatch_package();

    println!("total catatan log: {}", logger.total_entries());
}

Output yang Dihasilkan:

text
strong count setelah dua layanan: 3
total catatan log: 3

Mari kita bedah kode di atas:

  1. Baris 5 (entries: RefCell<Vec<String>>): isi logger adalah Vec<String> yang dibungkus RefCell — koleksi yang tumbuh, dijaga aturan pinjaman runtime. Mari kita perhatikan urutan tipe dari dalam ke luar: Vec<String> → RefCell<Vec<String>> pada field entries → SharedLogger yang memuat field tersebut → Rc<SharedLogger> yang dipegang tiap layanan.
  2. Baris 13-14 (fn write(&self, message: &str) … self.entries.borrow_mut()): penulisan hanya memegang penjaga RefMut selama satu statement. Method ini menerima &self, sehingga layanan mana pun yang memegang Rc bisa menulis tanpa kepemilikan &mut atas logger.
  3. Baris 23 dan 27 (logger: Rc<SharedLogger>): kedua layanan memegang tipe field yang sama — inilah deklarasi kepemilikan bersama. Pembangunan lewat new(logger: Rc<SharedLogger>) pada Baris 31 dan 41 memindahkan kepemilikan satu Rc ke masing-masing layanan.
  4. Baris 51 (let logger = Rc::new(...)): main menjadi pemilik pertama; reference count berangkat dari angka 1.
  5. Baris 53-54 (PaymentService::new(Rc::clone(&logger)) dst.): dua Rc::clone menaikkan penghitung menjadi 3 — dua layanan plus variabel logger di main. Tidak ada satu byte pun data log yang tersalin.
  6. Baris 58-60 (payment.receive_payment() dst.): tiga kejadian dicatat dari tiga pemanggilan berbeda. Setiap panggilan menulis melalui rantai penuh: Rc di-deref menuju SharedLogger, method write membuka RefCell sesaat, Vec tumbuh satu elemen, penjaga menutup.
  7. Baris 62 (logger.total_entries()): pemilik di main membaca hasil akhirnya — 3 catatan dari dua layanan yang berbeda, hidup rapi dalam satu Vec yang sama.

Data yang Mengalahkan Pemiliknya #

Pola ini punya sifat yang sempat mustahil di mata aturan ownership: data bisa melampaui umur pemilik asalnya. Fungsi berikut membangun logger secara lokal, memakainya, lalu mengembalikan layanan penerus — dan catatan yang ditulis sebelum fungsi usai tetap utuh:

rust
use std::cell::RefCell;
use std::rc::Rc;

struct AuditLog {
    entries: RefCell<Vec<String>>,
}

impl AuditLog {
    fn new() -> AuditLog {
        AuditLog { entries: RefCell::new(Vec::new()) }
    }

    fn record(&self, message: &str) {
        self.entries.borrow_mut().push(String::from(message));
    }

    fn size(&self) -> usize {
        self.entries.borrow().len()
    }
}

struct InventoryService {
    log: Rc<AuditLog>,
}

fn build_service() -> InventoryService {
    let log = Rc::new(AuditLog::new());

    let worker = InventoryService { log: Rc::clone(&log) };
    worker.log.record("pekerja memindai stok gudang");

    println!("strong count di dalam fungsi: {}", Rc::strong_count(&log));

    InventoryService { log: Rc::clone(&log) }
}

fn main() {
    let service = build_service();

    println!("strong count di main: {}", Rc::strong_count(&service.log));
    service.log.record("layanan baru menghitung stok");

    println!("total catatan yang bertahan: {}", service.log.size());
}

Output yang Dihasilkan:

text
strong count di dalam fungsi: 2
strong count di main: 1
total catatan yang bertahan: 2

Mari kita bedah kode di atas:

  1. Baris 27 (let log = Rc::new(AuditLog::new());): log adalah variabel lokal di dalam build_service — secara aturan lama, semua yang dimiliknya seharusnya mati saat fungsi selesai.
  2. Baris 29-30 (let worker = ...): pekerja sementara memegang Rc::clone, penghitung naik menjadi 2, dan catatan “pekerja memindai stok gudang” masuk ke log.
  3. Baris 34 (InventoryService { log: Rc::clone(&log) }): nilai kembalian memperoleh Rc ketiga. Saat fungsi berakhir, worker dan log dijatuhkan — penghitung turun dari 3 ke 1 — tetapi angka belum mencapai nol, sehingga AuditLog beserta seluruh catatannya tetap hidup di heap.
  4. Baris 40 (Rc::strong_count(&service.log)): penghitung kini tinggal satu, dipegang service — pemilik terakhir sekaligus satu-satunya. Data berpindah tangan dari fungsi ke pemanggil tanpa penyalinan.
  5. Baris 41-43: layanan penerus menulis catatan kedua dan membaca totalnya: 2. Catatan pertama yang ditulis jauh di dalam build_service ikut terhitung — bukti data dan pemilik asalnya sudah tidak terikat lagi.

Harga yang Dibayar Pola Ini #

Kenyamanan Rc<RefCell<T>> datang dengan tiga tagihan. Pertama, pemeriksaan pinjaman berpindah ke runtime: tabrakan pinjaman yang selama ini tertangkap kompilator kini menjadi panic yang hanya muncul pada jalur eksekusi tertentu. Kedua, setiap penulisan menambah pekerjaan kecil — pembukaan penjaga, pengecekan status, penutupan penjaga — yang pada kode panas terasa dampaknya. Ketiga, pola ini hanya sah untuk program satu thread: Rc dan RefCell sengaja tidak dirancang untuk dipakai lintas thread, dan pembagian data lintas thread menuntut mekanisme kunci yang kita bahas nanti di Section Koncurrency.

Ringkasan #

Rc<RefCell<T>> menggabungkan dua lapis mekanisme: Rc menghitung pemilik sehingga banyak layanan boleh berbagi satu data dan data boleh melampaui umur pemilik asalnya, sementara RefCell membuka jalur penulisan lewat &self dengan pemeriksaan pinjaman di runtime. Contoh logger dua layanan memperlihatkan pola lengkapnya — Rc::clone murah, penulisan lewat method berpenjaga singkat, strong_count naik-turun mengikuti daur hidup pemilik — dan catatan log bertahan melintasi batas fungsi penghasilnya. Kebangkitan kepemilikan bersama ini masih menyimpan satu penyakit bawaan: dua data yang saling memegang Rc tidak akan pernah mencapai angka nol. Penawarnya bernama Weak, dan itulah Chapter penutup section ini.