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:
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:
strong count printer: 2
strong count scanner: 2
main selesaiSesuatu 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:
- Baris 6 (
partner: RefCell<Option<Rc<Device>>>): field partner memakai tiga lapis —Optionkarena partner boleh kosong,Rckarena partner dimiliki bersama,RefCellkarena pasangan akan diisi setelah kedua perangkat dibuat. - Baris 19 (
*printer.partner.borrow_mut() = Some(Rc::clone(&scanner));): printer kini memegangRcmenuju scanner; strong count scanner naik menjadi 2. Baris 20 melakukan kebalikannya, dan strong count printer pun menjadi 2 — sama seperti yang dilaporkan output. - Setelah
println!("main selesai"): variabelscannerdijatuhkan, strong count printer turun dari 2 ke 1. Lalu variabelprinterdijatuhkan, strong count scanner turun dari 2 ke 1. Kedua penghitung berhenti di angka 1 — masing-masing ditopang oleh pasangannya yang tidak akan pernah mati. - Baris 11 yang tidak pernah tercetak: karena tidak ada penghitung yang mencapai nol,
Rctidak pernah menjalankanDropmilikDevice. 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:
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:
strong count printer: 1
weak count printer: 1
scanner terhubung dengan printer
main selesai
perangkat 'scanner' dimatikan
perangkat 'printer' dimatikanAlarm Drop yang gagal bunyi pada versi bocor kini berbunyi dua kali. Mari kita bedah kode di atas:
- Baris 6 (
partner: RefCell<Option<Weak<Device>>>): satu-satunya perubahan tipe —Rc<Device>menjadiWeak<Device>pada kedua arah, sehingga tidak ada lagi panah internal yang menambah strong count. - Baris 19-20 (
Rc::downgrade(&scanner)/Rc::downgrade(&printer)):downgrademenghasilkanWeaktanpa menaikkan strong count — buktinya pada output, strong count printer hanya 1 (dari variabelprinterdimain), sementara weak count printer bernilai 1 (dari fieldpartnermilik scanner). - Baris 25 (
if let Some(weak) = scanner.partner.borrow().as_ref()): patternif letmembongkarOptiondi dalamRefCell— pemegangWeakdiperoleh sebagai pinjaman singkat lewatborrow(). - Baris 26 (
if let Some(partner) = weak.upgrade()):upgrademencoba menukarWeakmenjadiRc. HasilnyaSomekarena printer masih hidup, danRcsementara yang dihasilkan menaikkan strong count printer menjadi 2 hanya selama blokif letberlangsung. - Dua baris terakhir output: saat
mainusai,scannerdijatuhkan — strong count scanner mencapai nol,Dropberjalan, “perangkat ‘scanner’ dimatikan” tercetak. Setelah ituprinterdijatuhkan, strong count-nya pun mencapai nol, dan alarm kedua berbunyi.Weakyang tersimpan di field partner masing-masing berubah menjadi pegangan mati yangupgrade-nya mengembalikanNone— 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:
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:
sensor 'sensor suhu ruangan' masih hidup
sensor 'sensor suhu ruangan' berhenti
sensor sudah berhentiMari kita bedah kode di atas:
- Baris 15 (
let handle: Weak<Sensor> = Rc::downgrade(&sensor);):handleadalah pegangan non-milik — variabel ini hidup lebih lama dari data yang ditunjuknya, dan tidak ada yang keberatan karena strong count tidak tersentuh. - Baris 17-20 (
match handle.upgrade()pertama): sensor masih dipegang variabelsensor, sehinggaupgrademengembalikanSomeyang membawaRcsegar; cabangSomemencetak nama sensor lewatlive.namedengan auto-deref biasa. - Baris 22 (
drop(sensor);): pemilik terakhir melepas kepemilikan secara eksplisit — strong count jatuh ke nol, methodDropmilikSensorberjalan seketika (baris kedua output), dan memori heap sensor dibebaskan meskipunhandlemasih ada. - Baris 24-27 (
match handle.upgrade()kedua):upgradekini mengembalikanNone— cabangNoneyang menyapa.handlesendiri tetap sah dipakai; pemegangWeaktidak 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 denganWeak— 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,upgrademengembalikanNonedan pemetaan tinggal dibersihkan. - Daftar pelanggan kejadian (listener): pusat kejadian memegang pelanggan dengan
Weaksehingga 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.