Rust: Ownership & Borrow Checker Tanpa Frustrasi
Status versi (terverifikasi 5 Nov 2026): stable 1.99.1, beta 1.100.0 (rilis 12 Nov 2026), nightly 1.101.0. Sumber: releases.rs + blog.rust-lang.org.
๐ Topik
Ownership model + NLL borrow checker (yang kamu pakai tiap hari), plus Polonius Alpha โ borrow checker generasi baru yang sudah default di nightly sejak 4 Agustus 2026 dan ditargetkan stabil sebelum akhir 2026.
Alasan topik ini relevan sekarang, bukan 2020: sebagian besar "borrow checker menyebalkan" yang kamu alami sebenarnya keterbatasan NLL, dan itu sedang diperbaiki.
๐ฏ Tujuan Belajar
Setelah sesi ini kamu bisa:
1. Menjelaskan kenapa fn get_mut_or_default gagal di NLL tapi lolos di Polonius โ tanpa hafal, dari prinsip.
2. Memilih antara &T, &mut T, Box, Rc, Arc, Cow berdasarkan ownership transfer, bukan feeling.
3. Refactor kode yang "ngotot gak mau compile" pakai 5 pattern standar (split borrow, NLL-friendly return, interior mutability, arena, callback/inversion).
4. Baca borrow error dan langsung tahu apakah itu bug kode kamu atau limitasi compiler.
๐๏ธ Arsitektur & Alur Teknis
Level 1 โ Ownership (model memori, bukan GC)
Tiga aturan, tidak berubah sejak Rust 1.0:
1. Setiap value punya tepat satu owner.
2. Cuma boleh satu owner aktif; pindah owner = move.
3. Owner drop โ value drop (deterministik, bukan GC).
Konsekuensi operasional: tidak ada use-after-free, tidak ada double-free, tidak ada data race โ semuanya ditolak saat compile, bukan runtime. Harga: kamu harus eksplisit soal siapa yang punya apa.
Level 2 โ Borrowing (pinjam, jangan ambil)
&T : shared borrow, boleh banyak, read-only
&mut T : exclusive borrow, cuma satu, read+write
Aturan alias: satu &mut XOR banyak &, dan referensi harus selalu hidup lebih pendek dari owner-nya (lifetime).
Level 3 โ Borrow checker: NLL vs Polonius
| NLL (stable 1.99) | Polonius Alpha (nightly, target stabil akhir 2026) | |
|---|---|---|
| Analisis | flow-insensitive terhadap outlives | flow-sensitive |
| Titik lemah | borrow dianggap hidup sepanjang region return, walau cabang lain tidak memakainya | tahu borrow mati di cabang yang tidak memakainya |
| Biaya compile | baseline | โฅ NLL, worst case teramati 2โ3ร lebih lambat |
| Regresi terukur | โ | dari top-10k crate crates.io, sangat sedikit yang lewat threshold 1% (data tim Polonius, blog 4 Agu 2026) |
| Status | default | default di nightly; opt-out -Zpolonius=off |
Penting โ klasifikasi kejujuran: Polonius Alpha disebut tim Rust sebagai "subset" tapi bukan subset sejati. Ada kode yang compile di Polonius Alpha tapi tidak di legacy Polonius, dan sebaliknya. Jadi: jangan asumsikan "Polonius = semua valid code lolos".
๐ป Kode Nyata
Toolchain: rustc 1.99.1 (stable). Contoh pertama jalan di stable; kedua butuh nightly.
1. Kasus klasik yang gagal di NLL
use std::collections::HashMap;
use std::hash::Hash;
// โ GAGAL di NLL (stable 1.99): E0499 / E0502
// โ
LOLOS di Polonius Alpha (nightly 1.101)
fn get_mut_or_default<'r, K, V>(map: &'r mut HashMap<K, V>, key: K) -> &'r mut V
where
K: Hash + Eq + Copy,
V: Default,
{
match map.get_mut(&key) {
Some(value) => value, // borrow dari sini dianggap hidup
None => { // ...sampai sini juga (NLL salah)
map.insert(key, V::default());
map.get_mut(&key).unwrap()
}
}
}
Kenapa NLL gagal: return type &'r mut V memaksa region 'r hidup sepanjang fungsi. NLL tidak melacak bahwa borrow dari get_mut mati di cabang None. Polonius flow-sensitive, jadi tahu.
2. Verifikasi behaviour
# stable โ expect error
rustc --edition 2021 t.rs
# nightly โ expect pass
rustup toolchain install nightly
rustup run nightly cargo +nightly build
# opt-out kalau kena regresi
RUSTFLAGS=-Zpolonius=off cargo +nightly build
3. Workaround di stable (kalau kamu gak mau nunggu)
Entry API โ ini yang seharusnya kamu pakai di production hari ini:
fn get_mut_or_default<'r, K, V>(map: &'r mut HashMap<K, V>, key: K) -> &'r mut V
where K: Hash + Eq + Copy, V: Default {
map.entry(key).or_default()
}
Lebih pendek, satu hash lookup (bukan dua), dan benar secara semantik. Pelajaran: banyak "borrow checker problem" hilang kalau kamu pilih API yang tepat.
4. Split borrow โ pattern paling sering dipakai
struct Ctx { users: Vec<String>, log: Vec<String> }
// โ &mut self di dua method โ double mutable borrow
// โ
split field
impl Ctx {
fn touch(&mut self, u: String) {
let Self { users, log } = self; // destructure = disjoint borrow
users.push(u.clone());
log.push(format!("add {u}"));
}
}
5. Interior mutability โ saat aturan alias benar-benar gak cocok
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node { val: i32, next: Option<Rc<RefCell<Node>>> }
let a = Rc::new(RefCell::new(Node { val: 1, next: None }));
let b = Rc::new(RefCell::new(Node { val: 2, next: Some(a.clone()) }));
// graph dengan back-edge: ownership tunggal tidak mungkin, Rc+RefCell ya
Trade-off: kehilangan compile-time safety untuk aliasing โ RefCell panic kalau double-borrow runtime. Untuk concurrent: Arc<Mutex<T>> โ kosnya lock, plus potensi deadlock.
6. Cow โ hindari clone di hot path
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') { Cow::Owned(s.replace(' ', "_")) }
else { Cow::Borrowed(s) } // zero alloc di kasus umum
}
๐ Angka & Benchmark
Yang terverifikasi dari blog tim Rust (4 Agustus 2026): - Analisis top 10.000 crate berdasarkan download di crates.io: regresi signifikan "relatif sedikit". - Worst case di luar top-10k: 2โ3ร compile time lebih lambat. - Threshold "signifikan" yang dipakai tim: regresi 1%. - Tujuan stabilisasi: sebelum akhir 2026.
Yang BELUM PASTI: tidak ada benchmark resmi soal runtime impact (teoretis nol โ ini analisis compile-time murni, bukan perubahan codegen). Kalau ada yang klaim "Polonius bikin binary lebih cepat", itu belum terbukti.
โ๏ธ Trade-off & Alternatif
| Pendekatan | Kapan pakai | Biaya |
|---|---|---|
&T / &mut T |
default, 90% kasus | harus disiplin lifetime |
| Split borrow (destructure) | method yang butuh mutasi 2 field | kode sedikit lebih verbose |
| Entry API / iterator API | hindari borrow-then-reinsert | tergantung API tersedia |
Rc<RefCell<T>> |
graph, tree dengan back-pointer, single-thread | runtime borrow check, panic risk, bukan Send |
Arc<Mutex<T>> |
shared mutable antar thread | lock contention, deadlock |
Arena (bumpalo, slotmap) |
struktur banyak node, lifetime seragam | delete granular tidak mungkin |
unsafe + raw pointer |
FFI, data structure performa kritis | hilang jaminan, harus audit sendiri |
| Clone | kalau data kecil & jarang | alokasi; paling sering "solusi" yang bikin kode lambat |
Aturan praktis: kalau clone() menyelesaikan borrow error dan data < beberapa ratus byte serta bukan di loop panas โ clone. Kalau di loop panas โ pikirkan ulang desainnya.
๐ญ Use Case Produksi & Failure Mode Umum
Failure mode #1 โ "borrow checker terlalu bodoh" yang sebenarnya bug logika kamu. Kalau kamu butuh &mut dua kali ke data yang sama, biasanya berarti operasi itu sebenarnya dua fase dan harus dipecah. Compiler cuma lebih cepat sadar.
Failure mode #2 โ RefCell panic di production. Pola umum: iterate borrow() sambil panggil callback yang borrow_mut(). Tidak ketahuan di test kalau test-nya tidak cover callback path. Mitigasi: ganti ke indeks-based atau try_borrow_mut() dengan error handling.
Failure mode #3 โ Arc<Mutex<T>> di mana-mana. Muncul di kode hasil translasi dari Java/C#. Hasilnya: lock contention, throughput turun. Sering bisa diganti channel (std::sync::mpsc / crossbeam / tokio::sync::mpsc) atau sharding state.
Failure mode #4 โ lifetime generic yang membengkak. Kalau signature fungsi punya 4+ lifetime parameter, itu sinyal desain ownership salah โ biasanya butuh borrow long-lived (arena) atau ownership transfer.
Use case nyata: hyper/tokio service hampir selalu bisa hidup tanpa Rc/RefCell sama sekali โ semua shared state lewat Arc di boundary, sisanya & / &mut. Kalau service kamu penuh interior mutability, itu aroma masalah desain.
๐ ๏ธ Latihan Praktis Kecil
cargo new --bin borrow_lab && cd borrow_lab
Isi src/main.rs dengan 3 blok ini, compile satu-satu, perhatikan error-nya:
fn main() {
// A. NLL limit โ gagal di stable
// fn get_mut_or_default(...) -> paste versi di atas, coba compile
// B. split borrow โ harus lolos
struct Ctx { a: Vec<u8>, b: Vec<u8> }
let mut c = Ctx { a: vec![], b: vec![] };
let Ctx { a, b } = &mut c;
a.push(1); b.push(2);
// C. RefCell runtime panic โ jalankan, lihat panic message
use std::cell::RefCell;
let cell = RefCell::new(0);
let _r1 = cell.borrow_mut();
// let _r2 = cell.borrow(); // uncomment -> panic "already mutably borrowed"
}
Lalu: rustup run nightly rustc src/main.rs --edition 2021 untuk blok A โ harus lolos.
Target waktu: 15 menit. Kalau blok A lolos di nightly dan gagal di stable, kamu sudah membuktikan sendiri perbedaan flow-sensitivity.
โ Cek Pemahaman
- Kenapa NLL menolak
get_mut_or_defaultpadahal tidak ada dua&mutyang hidup bersamaan secara runtime? Kata kunci: apa arti flow-insensitive dalam konteks region outlives. - Kamu punya graph dengan back-pointer dan butuh mutable traversal single-thread. Pilih
Rc<RefCell>, arena + index, atauunsafe? Sebutkan satu failure mode dari pilihanmu. - Polonius Alpha diketahui punya regresi compile 2โ3ร di kasus tertentu, dan belum tentu lolos semua kode yang lolos di legacy Polonius. Kalau punya CI monorepo besar, apa yang kamu ukur sebelum migrasi?
๐ Poin Kunci
- Ownership = compile-time memory management, bukan syntax.
- Borrow errors: kebanyakan bug desain, sedikit limitasi compiler. Polonius memperbaiki sebagian dari yang terakhir.
- Polonius Alpha default di nightly 1.101 sejak 4 Agu 2026; target stabil akhir 2026, belum terkonfirmasi resmi per hari ini. Jangan klaim sudah stabil di 1.99.
- Di stable sekarang: pakai Entry API untuk kasus
get_mut_or_defaultโ lebih benar dan lebih cepat dari workaround apa pun. - Urutan pilihan:
&/&mutโ split borrow โ API yang tepat โCowโ arena โArc<Mutex>โ clone โunsafe. Dari atas ke bawah.
โก๏ธ Eksplorasi Lanjut
- Polonius internals โ kenapa flow-sensitivity mahal:
smallcultfollowing.com/babystepsseries "Polonius revisited". -Znext-solver=globallyโ sedang dalam PR stabilisasi (#164209); ini mengubah cara trait resolution bekerja, dampak lebih luas dari Polonius.- Arena design pattern โ pelajari
bumpalo+slotmap; ini "borrow checker escape hatch" yang paling sering benar di production. - Custom allocators โ disebut di release notes 1.99 sebagai alasan deprecation pola "round-trip unleaking after
Box::leak". Baca dulu sebelum pakaiBox::leakdi kode baru. - Session berikut (kandidat):
Rust async: Pin, Future, dan kenapa tokio bedaโ Pin adalah kasus khusus dari borrow + move yang sering bikin frustrasi, melanjutkan topik ini secara natural.