Janji Rc terdengar sempurna: data mati tepat ketika pemilik terakhir melepaskannya. Namun ada satu penyakit bawaan yang menggerogoti janji itu secara diam-diam — dua nilai yang sama-sama memegang Rc satu sama lain. Ketika printer menyimpan Rc menuju scanner dan scanner menyimpan Rc menuju printer, reference count keduanya berhenti pada angka satu selamanya: tidak ada yang berani menjadi pemilik terakhir. Data tidak pernah dijatuhkan, method Drop tidak pernah berjalan, dan memori heap bocor pelan-pelan — memory leak di tengah bahasa yang mengklaim aman memori. Rust mengakui kenyataan ini dan menyediakan penawarnya: Weak<T>, weak reference yang menunjuk ke data tanpa menambah suara di penghitung pemilik. Chapter penutup section ini membedah cara siklus terbentuk, cara Weak memutusnya, dan cara Weak::upgrade mengembalikan kepemilikan dalam bungkus Option.

Masalah: Siklus Reference Count #

Kita bangun siklusnya secara sengaja. Dua perangkat saling menyimpan partner sebagai Option<Rc<Device>> di dalam RefCell — pola Rc<RefCell<...>> dari Chapter sebelumnya persis:

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

struct Device {
    name: String,
    partner: RefCell<Option<Rc<Device>>>,
}

impl Drop for Device {
    fn drop(&mut self) {
        println!("perangkat '{}' dimatikan", self.name);
    }
}

fn main() {
    let printer = Rc::new(Device { name: String::from("printer"), partner: RefCell::new(None) });
    let scanner = Rc::new(Device { name: String::from("scanner"), partner: RefCell::new(None) });

    *printer.partner.borrow_mut() = Some(Rc::clone(&scanner));
    *scanner.partner.borrow_mut() = Some(Rc::clone(&printer));

    println!("strong count printer: {}", Rc::strong_count(&printer));
    println!("strong count scanner: {}", Rc::strong_count(&scanner));
    println!("main selesai");
}

Output yang Dihasilkan:

text
strong count printer: 2
strong count scanner: 2
main selesai

Sesuatu yang penting justru hilang dari output itu: tidak ada satu pun baris “perangkat … dimatikan”, padahal method Drop pada Baris 9-13 sengaja dipasang sebagai alarm. Mari kita bedah kode dan peristiwanya:

  1. Baris 6 (partner: RefCell<Option<Rc<Device>>>): field partner memakai tiga lapis — Option karena partner boleh kosong, Rc karena partner dimiliki bersama, RefCell karena pasangan akan diisi setelah kedua perangkat dibuat.
  2. Baris 19 (*printer.partner.borrow_mut() = Some(Rc::clone(&scanner));): printer kini memegang Rc menuju scanner; strong count scanner naik menjadi 2. Baris 20 melakukan kebalikannya, dan strong count printer pun menjadi 2 — sama seperti yang dilaporkan output.
  3. Setelah println!("main selesai"): variabel scanner dijatuhkan, strong count printer turun dari 2 ke 1. Lalu variabel printer dijatuhkan, strong count scanner turun dari 2 ke 1. Kedua penghitung berhenti di angka 1 — masing-masing ditopang oleh pasangannya yang tidak akan pernah mati.
  4. Baris 11 yang tidak pernah tercetak: karena tidak ada penghitung yang mencapai nol, Rc tidak pernah menjalankan Drop milik Device. Kedua perangkat beserta seluruh isinya tetap menempati heap sampai proses program berakhir — inilah memory leak.

Siklus yang terbentuk dapat digambarkan sebagai berikut:

Diagram di atas memperlihatkan mengapa penghitung macet: empat panah Rc mengalir — dua dari variabel di main, dua dari field partner — dan saat dua panah dari main putus di akhir scope, dua panah internal tetap saling menopang. Setiap perangkat menjadi alasan hidup bagi pasangannya, tanpa titik akhir. Keamanan memori Rust tetap terjaga (tidak ada pointer menggantung, tidak ada akses ilegal), tetapi keamanan bukan berarti bebas kebocoran.

Mekanisme Weak: Reference Tanpa Suara #

Weak<T> dibuat lewat Rc::downgrade(&rc) dan hanya bisa menunjuk data yang sudah memiliki Rc. Perbedaan mendasarnya: Weak tidak menambah strong count — kehadirannya dicatat di penghitung terpisah bernama weak count yang bisa dibaca lewat Rc::weak_count, dan penghitung ini tidak berpengaruh pada keputusan hidup-mati data. Data tetap dijatuhkan ketika strong count mencapai nol, terlepas dari berapa banyak Weak yang masih menunjuk.

Harga dari kepemilikan yang tidak dihitung: Weak tidak bisa dipakai membaca data secara langsung, karena data mungkin sudah mati. Satu-satunya pintu masuknya adalah method upgrade() yang mengembalikan Option<Rc<T>> — Some berarti data masih hidup dan kita memperoleh Rc baru (strong count naik satu selama Rc itu dipegang), None berarti data sudah dijatuhkan dan weak reference tinggal menjadi kenangan. Desain Option ini memaksa setiap pemegang Weak menghadapi kematian data secara eksplisit — tidak ada jalan menuju dangling reference.

Memutus Siklus dengan Weak #

Solusinya sederhana secara struktur: satu arah dari siklus tetap memakai Rc sebagai pemilik, sedangkan arah baliknya diturunkan menjadi Weak. Simpul anak memegang orang tua lewat Weak, orang tua memegang anak lewat Rc — atau dalam kasus kita: scanner memegang printer sebagai hubungan non-milik:

rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Device {
    name: String,
    partner: RefCell<Option<Weak<Device>>>,
}

impl Drop for Device {
    fn drop(&mut self) {
        println!("perangkat '{}' dimatikan", self.name);
    }
}

fn main() {
    let printer = Rc::new(Device { name: String::from("printer"), partner: RefCell::new(None) });
    let scanner = Rc::new(Device { name: String::from("scanner"), partner: RefCell::new(None) });

    *printer.partner.borrow_mut() = Some(Rc::downgrade(&scanner));
    *scanner.partner.borrow_mut() = Some(Rc::downgrade(&printer));

    println!("strong count printer: {}", Rc::strong_count(&printer));
    println!("weak count printer: {}", Rc::weak_count(&printer));

    if let Some(weak) = scanner.partner.borrow().as_ref() {
        if let Some(partner) = weak.upgrade() {
            println!("scanner terhubung dengan {}", partner.name);
        }
    }

    println!("main selesai");
}

Output yang Dihasilkan:

text
strong count printer: 1
weak count printer: 1
scanner terhubung dengan printer
main selesai
perangkat 'scanner' dimatikan
perangkat 'printer' dimatikan

Alarm Drop yang gagal bunyi pada versi bocor kini berbunyi dua kali. Mari kita bedah kode di atas:

  1. Baris 6 (partner: RefCell<Option<Weak<Device>>>): satu-satunya perubahan tipe — Rc<Device> menjadi Weak<Device> pada kedua arah, sehingga tidak ada lagi panah internal yang menambah strong count.
  2. Baris 19-20 (Rc::downgrade(&scanner) / Rc::downgrade(&printer)): downgrade menghasilkan Weak tanpa menaikkan strong count — buktinya pada output, strong count printer hanya 1 (dari variabel printer di main), sementara weak count printer bernilai 1 (dari field partner milik scanner).
  3. Baris 25 (if let Some(weak) = scanner.partner.borrow().as_ref()): pattern if let membongkar Option di dalam RefCell — pemegang Weak diperoleh sebagai pinjaman singkat lewat borrow().
  4. Baris 26 (if let Some(partner) = weak.upgrade()): upgrade mencoba menukar Weak menjadi Rc. Hasilnya Some karena printer masih hidup, dan Rc sementara yang dihasilkan menaikkan strong count printer menjadi 2 hanya selama blok if let berlangsung.
  5. Dua baris terakhir output: saat main usai, scanner dijatuhkan — strong count scanner mencapai nol, Drop berjalan, “perangkat ‘scanner’ dimatikan” tercetak. Setelah itu printer dijatuhkan, strong count-nya pun mencapai nol, dan alarm kedua berbunyi. Weak yang tersimpan di field partner masing-masing berubah menjadi pegangan mati yang upgrade-nya mengembalikan None — tidak ada yang menggantung, tidak ada yang bocor.

upgrade Mengembalikan Option #

Perilaku upgrade saat data sudah mati perlu disaksikan langsung, karena di sinilah Weak berbeda total dari Rc:

rust
use std::rc::{Rc, Weak};

struct Sensor {
    name: String,
}

impl Drop for Sensor {
    fn drop(&mut self) {
        println!("sensor '{}' berhenti", self.name);
    }
}

fn main() {
    let sensor = Rc::new(Sensor { name: String::from("sensor suhu ruangan") });
    let handle: Weak<Sensor> = Rc::downgrade(&sensor);

    match handle.upgrade() {
        Some(live) => println!("sensor '{}' masih hidup", live.name),
        None => println!("sensor sudah berhenti"),
    }

    drop(sensor);

    match handle.upgrade() {
        Some(live) => println!("sensor '{}' masih hidup", live.name),
        None => println!("sensor sudah berhenti"),
    }
}

Output yang Dihasilkan:

text
sensor 'sensor suhu ruangan' masih hidup
sensor 'sensor suhu ruangan' berhenti
sensor sudah berhenti

Mari kita bedah kode di atas:

  1. Baris 15 (let handle: Weak<Sensor> = Rc::downgrade(&sensor);): handle adalah pegangan non-milik — variabel ini hidup lebih lama dari data yang ditunjuknya, dan tidak ada yang keberatan karena strong count tidak tersentuh.
  2. Baris 17-20 (match handle.upgrade() pertama): sensor masih dipegang variabel sensor, sehingga upgrade mengembalikan Some yang membawa Rc segar; cabang Some mencetak nama sensor lewat live.name dengan auto-deref biasa.
  3. Baris 22 (drop(sensor);): pemilik terakhir melepas kepemilikan secara eksplisit — strong count jatuh ke nol, method Drop milik Sensor berjalan seketika (baris kedua output), dan memori heap sensor dibebaskan meskipun handle masih ada.
  4. Baris 24-27 (match handle.upgrade() kedua): upgrade kini mengembalikan None — cabang None yang menyapa. handle sendiri tetap sah dipakai; pemegang Weak tidak pernah memegang pointer menggantung.

Kapan Memakai Weak #

Pedoman praktis dari pola yang barusan kita buktikan:

  • Hubungan orang tua-anak pada pohon data: orang tua memegang anak dengan Rc, anak menunjuk orang tua dengan Weak — pohon bisa dibongkar dari akar tanpa ada cabang yang saling menopang.
  • Cache dan pemetaan sementara: pemetaan memegang data dengan Weak; jika data asli sudah dibebaskan, upgrade mengembalikan None dan pemetaan tinggal dibersihkan.
  • Daftar pelanggan kejadian (listener): pusat kejadian memegang pelanggan dengan Weak sehingga pelanggan yang sudah mati tidak menghalangi pembersihan pusatnya.

Ringkasan #

Siklus Rc membuat strong count kedua nilai terkunci di angka di atas nol sehingga Drop tidak pernah berjalan — kebocoran memori yang sah di mata kompilator dan tidak membatalkan jaminan memory safety. Weak<T> memutus siklus tersebut: Rc::downgrade menghasilkan reference yang hanya menambah weak count, Rc::weak_count membaca penghitungnya, dan upgrade() menjadi satu-satunya pintu menuju data dengan hasil Option<Rc<T>> — Some saat data hidup, None setelah pemilik terakhir pergi. Dengan ini, peta smart pointer kita lengkap: Box untuk kepemilikan tunggal di heap, Deref dan Drop sebagai mesin transparansi dan pembersihan, Rc untuk kepemilikan bersama, RefCell untuk mutasi terjaga runtime, dan Weak sebagai jaring pengaman terhadap siklus. Section berikutnya kita meninggalkan satu thread dan menghadapi tantangan yang menuntut mekanisme baru: data yang benar-benar diakses banyak thread sekaligus.