โ† semua materi
15:00 Teknis ยท 05 Nov 2026 ยท 15:02

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

  1. Kenapa NLL menolak get_mut_or_default padahal tidak ada dua &mut yang hidup bersamaan secara runtime? Kata kunci: apa arti flow-insensitive dalam konteks region outlives.
  2. Kamu punya graph dengan back-pointer dan butuh mutable traversal single-thread. Pilih Rc<RefCell>, arena + index, atau unsafe? Sebutkan satu failure mode dari pilihanmu.
  3. 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


โžก๏ธ Eksplorasi Lanjut

  1. Polonius internals โ€” kenapa flow-sensitivity mahal: smallcultfollowing.com/babysteps series "Polonius revisited".
  2. -Znext-solver=globally โ€” sedang dalam PR stabilisasi (#164209); ini mengubah cara trait resolution bekerja, dampak lebih luas dari Polonius.
  3. Arena design pattern โ€” pelajari bumpalo + slotmap; ini "borrow checker escape hatch" yang paling sering benar di production.
  4. Custom allocators โ€” disebut di release notes 1.99 sebagai alasan deprecation pola "round-trip unleaking after Box::leak". Baca dulu sebelum pakai Box::leak di kode baru.
  5. 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.