Lewati ke konten

Weak dan Cow di Rust

Di halaman Smart pointer, kamu sudah berkenalan dengan Box, Rc, dan RefCell. Halaman ini menambahkan dua alat lagi yang mengatasi dua masalah yang sangat nyata. Weak<T> membuatmu bisa menunjuk ke sebuah nilai tanpa membuatnya tetap hidup. Cow<T> membuat sebuah fungsi bisa mengembalikan data pinjaman kalau memungkinkan, dan baru membuat salinan kalau memang terpaksa.

Anggap Rc seperti kunci rumah: selama masih ada yang memegang kunci, rumahnya tetap ada. Weak lebih mirip alamat yang ditulis di secarik kertas. Kamu bisa memakainya untuk menemukan rumah itu, tapi kalau semua orang sudah mengembalikan kuncinya dan rumahnya sudah tidak ada, alamat itu tidak mengarah ke mana-mana. Tidak ada yang terjebak membayar rumah yang sudah mereka lupakan.

Masalahnya: cycle Rc tidak pernah dibersihkan

Section titled “Masalahnya: cycle Rc tidak pernah dibersihkan”

Nilai Rc di-drop saat strong count (jumlah pemilik kuat) mencapai 0. Itu bekerja dengan baik, sampai ada dua nilai yang saling memegang Rc ke satu sama lain. Akibatnya, masing-masing membuat yang lain tetap hidup, selamanya. Inilah yang disebut reference cycle (siklus referensi).

Untuk melihatnya, tipe Person di bawah mengimplementasikan Drop, jadi ia mencetak pesan saat dibersihkan. Perhatikan pesan mana yang muncul:

use std::cell::RefCell;
use std::rc::Rc;
struct Person {
name: String,
friend: RefCell<Option<Rc<Person>>>,
}
impl Drop for Person {
fn drop(&mut self) {
println!("membersihkan {}", self.name);
}
}
fn main() {
let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) });
let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice menunjuk ke bob, dan bob menunjuk balik ke alice
*alice.friend.borrow_mut() = Some(Rc::clone(&bob));
*bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("strong count alice: {}", Rc::strong_count(&alice));
println!("strong count bob: {}", Rc::strong_count(&bob));
println!("main selesai");
}

Output:

strong count alice: 2
strong count bob: 2
main selesai
▶ Coba sendiri

Perhatikan apa yang tidak ada: tidak ada membersihkan alice dan tidak ada membersihkan bob. Saat main berakhir, variabel alice dan bob hilang, jadi setiap count turun dari 2 ke 1. Tapi count itu tidak pernah mencapai 0, karena setiap orang masih dipegang oleh field friend milik yang lain. Memori itu bocor (memory leak).

Jangan berkecil hati kalau ini mengejutkanmu. Compiler tidak bisa menangkapnya, dan ini bukan crash. Ini hanya memori yang tidak pernah kembali. Di program yang berjalan lama dan terus membuat cycle, jumlahnya terus menumpuk.

Weak<T>: pointer yang tidak membuat nilai tetap hidup

Section titled “Weak<T>: pointer yang tidak membuat nilai tetap hidup”

Solusinya adalah membuat salah satu atau kedua link itu menjadi weak (lemah). Kamu mendapatkan Weak dari sebuah Rc dengan Rc::downgrade. Link weak tidak dihitung sebagai pemilik, jadi ia tidak mencegah nilainya di-drop.

Karena nilainya mungkin sudah tidak ada, kamu tidak bisa memakai Weak secara langsung. Kamu memanggil .upgrade(), yang memberimu Option<Rc<T>>: Some kalau nilainya masih hidup, None kalau sudah tidak.

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Person {
name: String,
friend: RefCell<Weak<Person>>,
}
impl Drop for Person {
fn drop(&mut self) {
println!("membersihkan {}", self.name);
}
}
fn main() {
let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) });
let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob);
*bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() {
println!("teman alice adalah {}", friend.name);
}
println!("alice: strong = {}, weak = {}", Rc::strong_count(&alice), Rc::weak_count(&alice));
println!("main selesai");
}

Output:

teman alice adalah bob
alice: strong = 1, weak = 1
main selesai
membersihkan bob
membersihkan alice
▶ Coba sendiri

Kali ini kedua baris membersihkan muncul. Alice punya satu pemilik strong (variabel alice) dan satu link weak (dari Bob). Saat main berakhir, strong count-nya mencapai 0, dan Rust membersihkannya. Bob dibersihkan lebih dulu karena variabel di-drop dalam urutan terbalik dari urutan pembuatannya.

Beberapa hal yang perlu diingat:

  • Weak::new() membuat pointer weak kosong yang tidak menunjuk ke apa pun. upgrade() padanya selalu mengembalikan None. Ini nilai awal yang praktis.
  • Rc::strong_count menghitung pemilik. Rc::weak_count menghitung link weak. Hanya strong count yang menentukan kapan nilainya di-drop.
  • Rc yang kamu dapat dari upgrade() adalah pemilik sungguhan yang sifatnya sementara. Saat ia keluar dari cakupan (scope), strong count-nya turun lagi.

Tree folder: anak dimiliki, parent itu weak

Section titled “Tree folder: anak dimiliki, parent itu weak”

Tree (struktur pohon) adalah tempat klasik untuk Weak. Sebuah folder memiliki subfolder-subfoldernya, jadi children berisi Rc. Subfolder juga ingin tahu parent-nya, tapi ia tidak seharusnya memilikinya. Kalau kedua arah memakai Rc, kamu akan mendapat cycle lagi. Jadi parent adalah Weak:

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Folder {
name: String,
parent: RefCell<Weak<Folder>>,
children: RefCell<Vec<Rc<Folder>>>,
}
fn new_folder(name: &str) -> Rc<Folder> {
Rc::new(Folder {
name: name.to_string(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
fn full_path(folder: &Folder) -> String {
match folder.parent.borrow().upgrade() {
Some(parent) => format!("{}/{}", full_path(&parent), folder.name),
None => folder.name.clone(),
}
}
fn main() {
let home = new_folder("home");
let photos = new_folder("photos");
let trips = new_folder("trips");
add_child(&home, Rc::clone(&photos));
add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() {
Some(parent) => println!("photos ada di dalam {}", parent.name),
None => println!("photos tidak punya parent"),
}
println!("path lengkap: {}", full_path(&trips));
println!("home: strong = {}, weak = {}", Rc::strong_count(&home), Rc::weak_count(&home));
println!("photos: strong = {}, weak = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));
}

Output:

photos ada di dalam home
path lengkap: home/photos/trips
home: strong = 1, weak = 1
photos: strong = 2, weak = 1
▶ Coba sendiri

Baca count-nya pelan-pelan. photos punya dua pemilik strong: variabel photos dan daftar children di dalam home. Ia punya satu link weak: field parent milik trips. home hanya punya satu pemilik strong, yaitu variabelnya sendiri, karena tidak ada yang memiliki parent-nya. Itulah bentuk yang kamu inginkan: kepemilikan mengalir ke bawah tree, dan link yang mengarah ke atas bersifat weak.

RefCell ada di sana karena kamu mengisi parent dan menambah children setelah folder-foldernya sudah berada di dalam Rc, sedangkan Rc sendiri hanya memberi akses bersama yang read-only.

Daftar weak yang tidak membuat nilai tetap hidup

Section titled “Daftar weak yang tidak membuat nilai tetap hidup”

Weak juga sangat cocok untuk daftar “awasi ini, tapi jangan dipegang”: listener, observer, atau cache berisi barang milik orang lain. Di sini skor sebuah game mengirim update ke siapa pun yang masih mendengarkan. Saat sebuah listener di-drop, link weak-nya langsung berhenti bekerja:

use std::rc::{Rc, Weak};
struct Listener {
name: String,
}
fn notify(listeners: &[Weak<Listener>], event: &str) {
for weak in listeners {
match weak.upgrade() {
Some(listener) => println!("{} menerima: {event}", listener.name),
None => println!("(satu listener sudah hilang, dilewati)"),
}
}
}
fn main() {
let screen = Rc::new(Listener { name: String::from("screen") });
let logger = Rc::new(Listener { name: String::from("logger") });
let mut listeners: Vec<Weak<Listener>> = Vec::new();
listeners.push(Rc::downgrade(&screen));
listeners.push(Rc::downgrade(&logger));
notify(&listeners, "skor 10");
drop(logger); // logger ditutup oleh pemiliknya
notify(&listeners, "skor 20");
// rapikan link yang sudah tidak menunjuk ke apa pun
listeners.retain(|weak| weak.upgrade().is_some());
println!("listener tersisa: {}", listeners.len());
}

Output:

screen menerima: skor 10
logger menerima: skor 10
screen menerima: skor 20
(satu listener sudah hilang, dilewati)
listener tersisa: 1
▶ Coba sendiri

Daftar itu tidak pernah menghalangi logger untuk di-drop. Kalau daftarnya memegang Rc, logger akan tetap hidup di dalam daftar, terus menerima pesan yang tidak diinginkan siapa pun. Dengan Weak, pemiliknya yang memutuskan kapan sesuatu hilang, dan yang lain cukup mengecek dengan upgrade().

Cow: pinjam kalau bisa, salin kalau terpaksa

Section titled “Cow: pinjam kalau bisa, salin kalau terpaksa”

Sekarang masalah yang berbeda. Misalnya kamu menulis fungsi yang merapikan username. Kebanyakan nama sudah rapi. Sebagian kecil punya spasi berlebih atau huruf kapital. Kalau fungsimu selalu mengembalikan String baru, kamu membuat salinan dari setiap nama, bahkan nama yang tidak kamu ubah.

Cow (“clone on write”, salin saat ditulis, dari std::borrow::Cow) mengatasi ini. Cow<'_, str> adalah enum dengan dua varian:

  • Cow::Borrowed(&str): “ini teks aslinya, tidak diubah”. Tanpa alokasi.
  • Cow::Owned(String): “aku harus mengubahnya, jadi ini String baru”.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> {
let trimmed = name.trim();
let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper {
Cow::Borrowed(name) // sudah rapi: kembalikan aslinya
} else {
Cow::Owned(trimmed.to_lowercase()) // perlu diperbaiki: buat String baru
}
}
fn main() {
for input in ["alice", " Bob ", "CAROL", "dave"] {
match normalize(input) {
Cow::Borrowed(name) => println!("{input:?} -> {name:?} (dipinjam, tanpa salinan)"),
Cow::Owned(name) => println!("{input:?} -> {name:?} (owned, String baru)"),
}
}
}

Output:

"alice" -> "alice" (dipinjam, tanpa salinan)
" Bob " -> "bob" (owned, String baru)
"CAROL" -> "carol" (owned, String baru)
"dave" -> "dave" (dipinjam, tanpa salinan)
▶ Coba sendiri

Dua dari empat nama sama sekali tidak memakan biaya. Di program sungguhan yang 99% input-nya sudah rapi, itu berarti banyak sekali salinan yang tidak perlu kamu buat. '_ di Cow<'_, str> hanya berarti “ini mungkin meminjam dari input”. Kalau lifetime masih terasa samar, halaman Lifetime menjelaskannya.

Biasanya kamu sama sekali tidak perlu melakukan match pada Cow. Ia melakukan deref ke &str, jadi kamu bisa memanggil method string padanya dan mengirim &name ke fungsi apa pun yang menerima &str. Saat kamu benar-benar butuh String milikmu sendiri, panggil .into_owned(). Method ini memindahkan String-nya keluar kalau memang ada, dan baru menyalin kalau Cow-nya berupa pinjaman.

use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> {
let trimmed = name.trim();
if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) {
Cow::Borrowed(name)
} else {
Cow::Owned(trimmed.to_lowercase())
}
}
fn greet(name: &str) {
println!("Selamat datang, {name}!");
}
fn main() {
let name = normalize(" Eve ");
println!("panjang: {}", name.len()); // method str langsung bisa dipakai
println!("diawali e: {}", name.starts_with('e'));
greet(&name); // &Cow<str> berubah menjadi &str
let saved: String = name.into_owned(); // sekarang jadi String biasa
println!("{saved:?} disimpan ke daftar pengguna");
}

Output:

panjang: 3
diawali e: true
Selamat datang, eve!
"eve" disimpan ke daftar pengguna
▶ Coba sendiri

Cow juga bisa dipakai dengan tipe lain, seperti Cow<'_, [i32]> untuk slice yang mungkin perlu diubah. Tapi untuk teks, Cow<'_, str> yang paling sering kamu temui.

Berikut contekan singkat untuk semua tipe mirip pointer yang sudah kamu temui sejauh ini:

Tipe Apa yang diberikannya Pakai saat
Box<T> Satu pemilik, nilai di heap Tipe rekursif, atau nilai besar yang ingin kamu pindahkan dengan murah
Rc<T> Banyak pemilik, satu thread Beberapa bagian programmu berbagi data yang sama
Arc<T> Banyak pemilik, lintas thread Sama seperti Rc, tapi dibagi antar-thread (lihat Shared state)
Weak<T> Link yang tidak memiliki Link ke parent, daftar observer, cache, memutus cycle Rc
RefCell<T> Mengubah nilai lewat referensi bersama, dicek saat runtime Kamu perlu mengubah sesuatu yang ada di dalam Rc
Cow<'_, T> Pinjaman atau milik sendiri, ditentukan saat runtime Fungsi yang biasanya mengembalikan input-nya tanpa diubah

Kalau tidak yakin, mulailah dengan ownership (kepemilikan) dan borrowing (peminjaman) biasa. Tambahkan tipe-tipe ini hanya saat compiler, atau sebuah memory leak, memberitahumu bahwa kamu membutuhkannya.

Memanggil unwrap pada hasil upgrade setelah nilainya hilang

Section titled “Memanggil unwrap pada hasil upgrade setelah nilainya hilang”

upgrade() mengembalikan Option bukan tanpa alasan. Kalau kamu memanggil .unwrap() karena kebiasaan, programmu akan panic begitu nilainya sudah di-drop:

use std::rc::Rc;
fn main() {
let session = Rc::new(String::from("user-42"));
let link = Rc::downgrade(&session);
drop(session); // sesinya berakhir
let current = link.upgrade().unwrap();
println!("masih login sebagai {current}");
}

Saat kamu menjalankannya, hasilnya:

thread 'main' (12345) panicked at src/main.rs:9:34:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Coba sendiri

Pesan panic ini artinya unwrap() dipanggil pada nilai None. Solusinya, perlakukan “nilainya sudah hilang” sebagai kasus yang wajar, dengan match atau if let:

match link.upgrade() {
Some(current) => println!("masih login sebagai {current}"),
None => println!("sesi sudah berakhir"),
}

Kalau anak memegang Rc<Parent> dan parent memegang Rc<Child>, kamu sudah membangun cycle seperti di bagian pertama. Tidak ada yang crash dan tidak ada peringatan; memorinya hanya tidak pernah kembali. Aturan yang bagus: tentukan siapa memiliki siapa, pakai Rc ke arah itu, dan Weak untuk setiap link ke arah sebaliknya.

struct Folder {
parent: RefCell<Rc<Folder>>, // salah: anak membuat parent tetap hidup
children: RefCell<Vec<Rc<Folder>>>,
}

Ubah parent menjadi RefCell<Weak<Folder>>, dan cycle-nya pun hilang.

1. Komentar di postingan yang sudah dihapus

Section titled “1. Komentar di postingan yang sudah dihapus”

Buat sebuah Post dengan title, disimpan di dalam Rc. Buat Comment dengan text dan post: Weak<Post>. Tulis fungsi describe(comment: &Comment) yang mencetak "<text>" di <title> kalau postingannya masih ada, atau "<text>" di postingan yang sudah dihapus kalau tidak. Panggil fungsinya, drop postingannya, lalu panggil lagi. Petunjuk: pakai match comment.post.upgrade().

Lihat solusi
use std::rc::{Rc, Weak};
struct Post {
title: String,
}
struct Comment {
text: String,
post: Weak<Post>,
}
fn describe(comment: &Comment) {
match comment.post.upgrade() {
Some(post) => println!("\"{}\" di {}", comment.text, post.title),
None => println!("\"{}\" di postingan yang sudah dihapus", comment.text),
}
}
fn main() {
let post = Rc::new(Post { title: String::from("Belajar Rust") });
let comment = Comment { text: String::from("Postingan bagus!"), post: Rc::downgrade(&post) };
describe(&comment);
drop(post);
describe(&comment);
}

Output:

"Postingan bagus!" di Belajar Rust
"Postingan bagus!" di postingan yang sudah dihapus
▶ Coba sendiri

Komentar itu tidak pernah membuat postingannya tetap hidup. Begitu satu-satunya Rc di-drop, upgrade() mengembalikan None, dan komentar itu menanganinya dengan tenang alih-alih panic.

Tulis fn escape(text: &str) -> Cow<'_, str> yang mengganti setiap < dengan &lt;. Kalau teksnya tidak mengandung <, kembalikan sebagai pinjaman. Uji dengan "halo dunia" dan "1 < 2", lalu cetak varian mana yang kamu dapat. Petunjuk: cek dulu dengan text.contains('<'), lalu pakai text.replace('<', "&lt;") untuk kasus owned.

Lihat solusi
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} else {
Cow::Borrowed(text)
}
}
fn main() {
for text in ["halo dunia", "1 < 2"] {
match escape(text) {
Cow::Borrowed(s) => println!("{s} (dipinjam)"),
Cow::Owned(s) => println!("{s} (owned)"),
}
}
}

Output:

halo dunia (dipinjam)
1 &lt; 2 (owned)
▶ Coba sendiri

Hanya teks yang benar-benar mengandung < yang perlu membayar String baru. Sisanya langsung lewat, tanpa diubah.

  • Dua nilai Rc yang saling menunjuk tidak pernah mencapai strong count 0, jadi keduanya bocor dan Drop-nya tidak pernah berjalan.
  • Rc::downgrade membuat Weak<T>, yang tidak membuat nilainya tetap hidup. upgrade() mengembalikan Option<Rc<T>>.
  • Di tree, anak dimiliki dengan Rc dan link ke parent berupa Weak. Kepemilikan mengalir ke bawah, link weak menunjuk ke atas.
  • Daftar observer dan cache bisa memegang Weak supaya tidak pernah membuat sesuatu tetap hidup secara tidak sengaja.
  • Cow<'_, str> mengembalikan teks pinjaman kalau tidak ada yang berubah, dan String owned hanya kalau ada yang berubah. Ia melakukan deref ke &str, dan .into_owned() memberimu String.

Kuis singkat

1. Dua nilai saling memegang Rc ke satu sama lain. Apa yang terjadi saat main berakhir?

2. Apa yang dikembalikan weak.upgrade()?

3. Di tree folder, link mana yang sebaiknya Weak?

4. Kapan fungsi yang mengembalikan Cow<'_, str> tidak perlu membuat String baru?

Skor: 0 / 4