Box menegaskan satu prinsip yang telah kita pegang sejak awal: setiap nilai hanya punya satu pemilik. Prinsip tersebut aman, tetapi dunia nyata sering menolaknya — aplikasi grafis memuat banyak komponen yang sama-sama menampilkan satu ikon, sistem dokumentasi menampung banyak bab yang merujuk satu glosarium, dan program apapun yang menampung hubungan berbagi data. Reference bisa meminjamkan akses, tetapi reference tidak memiliki: begitu pemilik aslinya mati, seluruh pinjaman ikut gugur, dan kita belum punya cara untuk memindahkan kepemilikan ke “beberapa tangan sekaligus”. Rust menjawab kebutuhan ini dengan Rc<T> — smart pointer bernama reference count yang menghitung jumlah pemiliknya dan hanya menjatuhkan data ketika pemilik terakhir melepaskannya.
Masalah: Satu Data, Dua Pemilik #
Kita mulai dari error klasik. Sebuah layar aplikasi kasir dan modul laporan harian sama-sama butuh mencantumkan nama brand yang sama, dan keduanya ingin menyimpan nama tersebut sebagai field miliknya sendiri:
struct MenuService {
brand: String,
}
fn main() {
let brand = String::from("Kopi Nusantara");
let display_service = MenuService { brand: brand };
let report_service = MenuService { brand: brand };
println!("{} dan {}", display_service.brand, report_service.brand);
}Pesan error kompilator:
error[E0382]: use of moved value: `brand`
--> main.rs:9:47
|
6 | let brand = String::from("Kopi Nusantara");
| ----- move occurs because `brand` has type `String`, which does not implement the `Copy` trait
7 |
8 | let display_service = MenuService { brand: brand };
| ----- value moved here
9 | let report_service = MenuService { brand: brand };
| ^^^^^ value used here after move
|
help: consider cloning the value if the performance cost is acceptable
|
8 | let display_service = MenuService { brand: brand.clone() };
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.Pesan E0382 menceritakan nasib brand secara rinci: kepemilikan berpindah ke display_service pada Baris 8, lalu report_service mencoba memakai brand yang sudah mati pada Baris 9. Perhatikan saran perbaikan dari kompilator — brand.clone() — yang memang sah tetapi membayar biaya menyalin seluruh teks dan menghasilkan dua data terpisah yang harus disinkronkan manual jika brand berganti. Yang kita inginkan bukan dua salinan, melainkan satu data dengan dua pemilik sah. Kebutuhan itulah yang tidak bisa dipenuhi String.
Mekanisme Rc: Penghitung Pemilik #
Rc<T> bekerja dengan menitipkan buku besar di samping datanya. Saat Rc::new(...) dipanggil, Rust mengalokasikan satu petak heap yang memuat dua hal: nilai T itu sendiri dan sebuah strong reference count — penghitung pemilik yang dimulai dari angka 1. Setiap Rc<T> yang beredar hanyalah pointer seukuran satu word menuju petak tersebut, sama rampingnya dengan Box.
Perbedaannya lahir pada dua peristiwa kunci. Pertama, Rc::clone(&rc) tidak menyalin data sama sekali — operasi ini hanya menaikkan angka penghitung satu tingkat lalu mengembalikan Rc baru yang menunjuk petak yang sama; perpindahan dan penyalinan data tidak terjadi. Kedua, saat sebuah Rc keluar dari scope, Rust tidak langsung menjatuhkan datanya — Rust menurunkan penghitung satu tingkat. Data benar-benar dijatuhkan hanya ketika penghitung mencapai nol, dan pada momen itulah Drop milik nilai berjalan. Karena itu, Rc::clone jauh lebih murah daripada clone pada data: satu operasi penambahan angka versus satu penyalinan penuh.
Membagi Kepemilikan dengan Rc::clone #
Mari kita tulis ulang program brand dengan kepemilikan bersama:
use std::rc::Rc;
struct MenuService {
brand: Rc<String>,
}
fn main() {
let brand = Rc::new(String::from("Kopi Nusantara"));
let display_service = MenuService { brand: Rc::clone(&brand) };
let report_service = MenuService { brand: Rc::clone(&brand) };
println!("layar menampilkan: {}", display_service.brand);
println!("laporan mencantumkan: {}", report_service.brand);
println!("jumlah pemilik brand: {}", Rc::strong_count(&brand));
}Output yang Dihasilkan:
layar menampilkan: Kopi Nusantara
laporan mencantumkan: Kopi Nusantara
jumlah pemilik brand: 3Mari kita bedah kode di atas:
- Baris 4 (
brand: Rc<String>): field kini bertipeRc<String>— bukanString— sehingga setiap pemegang field adalah pemilik sah lewat mekanisme penghitung. - Baris 8 (
let brand = Rc::new(...)): teks “Kopi Nusantara” naik ke heap bersama penghitung yang bernilai 1 — pemilik pertama adalah variabelbranddimain. - Baris 10-11 (
Rc::clone(&brand)): kedua struct memperolehRc-nya lewatRc::clone— tidak ada teks yang disalin, hanya penghitung yang naik dua kali menjadi 3. Perhatikan arah argument-nya:Rc::clonemenerima reference menujuRcyang sudah ada, bukan nilaiRcitu sendiri. - Baris 13-14 (
display_service.brand): pembacaan isi tetap transparan —RcmengimplementasikanDerefdenganTarget = T, sehingga makroprintln!mengikuti pointer ke heap tanpa perintah tambahan. - Baris 15 (
Rc::strong_count(&brand)): fungsi pembaca penghitung membuktikan jumlah pemilik saat ini: variabelbrand,display_service, danreport_service— total 3.
Mengamati strong_count dan Momen Data Mati #
Penghitung ini bisa kita amati dari waktu ke waktu, dan dengan bantuan trait Drop dari Chapter sebelumnya, kita bisa membidik persis kapan data dijatuhkan:
use std::rc::Rc;
struct SharedLogger {
label: String,
}
impl Drop for SharedLogger {
fn drop(&mut self) {
println!("logger '{}' dimatikan", self.label);
}
}
fn main() {
let logger = Rc::new(SharedLogger { label: String::from("aplikasi kasir") });
println!("pemilik awal: {}", Rc::strong_count(&logger));
let cashier_screen = Rc::clone(&logger);
println!("setelah layar kasir memegang: {}", Rc::strong_count(&logger));
let daily_report = Rc::clone(&logger);
println!("setelah laporan harian memegang: {}", Rc::strong_count(&logger));
println!("label terbaca lewat laporan: {}", daily_report.label);
drop(cashier_screen);
println!("setelah layar kasir dilepas: {}", Rc::strong_count(&logger));
}Output yang Dihasilkan:
pemilik awal: 1
setelah layar kasir memegang: 2
setelah laporan harian memegang: 3
label terbaca lewat laporan: aplikasi kasir
setelah layar kasir dilepas: 2
logger 'aplikasi kasir' dimatikanMari kita bedah kode di atas:
- Baris 7-10 (
impl Drop for SharedLogger): methoddropsengaja mencetak pesan sehingga momen kematian data terlihat telanjang di output. - Baris 16-24 (urutan angka 1 → 2 → 3): setiap
Rc::clonemenaikkan penghitung tepat satu — angka pada blok output mengikuti jumlah pemilik yang sedang beredar. - Baris 25 (
drop(cashier_screen);): fungsistd::mem::dropdari Chapter Trait Drop mematikan satu pemilik lebih awal. Penghitung turun dari 3 menjadi 2, dan karena angka masih di atas nol,SharedLoggersama sekali belum dijatuhkan — tidak ada pesan “dimatikan” setelah baris ini. - Baris terakhir output (
logger 'aplikasi kasir' dimatikan): penghitung mencapai nol justru di akhir scopemain— variabeldaily_reportdijatuhkan lebih dulu (penghitung 2 → 1), laluloggersendiri (1 → 0), dan barulah methoddropmilikSharedLoggerberjalan. Data hidup selama masih ada satu saja pemilik yang tersisa.
Rc Adalah Kepemilikan Read-Only #
Satu batasan besar menunggu di ujung bab ini. Data yang dimiliki Rc tidak bisa diubah, karena Rc sengaja hanya mengimplementasikan Deref tanpa DerefMut — persis situasi yang diperingatkan Chapter Deref dan DerefMut:
use std::rc::Rc;
fn main() {
let notes = Rc::new(String::from("daftar belanja"));
notes.push_str(" tambahan");
println!("{}", notes);
}Pesan error kompilator:
error[E0596]: cannot borrow data in an `Rc` as mutable
--> main.rs:6:5
|
6 | notes.push_str(" tambahan");
| ^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<String>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0596`.Pesan E0596 menyebut akar masalahnya secara gamblang: DerefMut tidak diimplementasikan untuk Rc<String>. Larangan ini bukan kelemahan, melainkan konsekuensi logis kepemilikan bersama: push_str menuntut &mut String, dan aturan borrowing hanya mengizinkan &mut ketika tidak ada pemegang reference lain. Dengan tiga pemilik yang bebas memegang Rc::clone kapan pun, kompilator tidak akan pernah bisa membuktikan ketentuan itu saat kompilasi — maka Rust memilih jalan aman: menutup rapat jalur mutasi sama sekali. Membaca lewat banyak pemilik diizinkan; menulis lewat banyak pemilik, dalam bentuk apapun, tidak.
Ringkasan #
Rc<T> mengubah kepemilikan tunggal menjadi kepemilikan terhitung: satu petak heap menyimpan data beserta strong reference count, Rc::clone(&rc) menambah pemilik tanpa menyalin data, Rc::strong_count membocorkan jumlah pemilik saat ini, dan data baru dijatuhkan ketika penghitung menciut ke nol — momen yang kita tangkap basah lewat method Drop. Error E0382 memperlihatkan kenapa kepemilikan bersama dibutuhkan, sementara error E0596 memperlihatkan harga yang harus dibayar: data Rc terkunci read-only. Dua kebutuhan itu — banyak pemilik dan bisa diubah — akhirnya bisa dipertemukan, tetapi konsekuensinya berpindah dari kompilasi ke runtime. Mesin pengubahnya bernama RefCell, dan itulah Chapter berikutnya.