Borrowing & References — Rust

Borrowing & References di Rust Daripada memindahkan ownership, kita bisa meminjam (borrow) value menggunakan references. Ini adalah cara paling umum untuk…

Borrowing (meminjam) adalah cara memakai sebuah nilai tanpa mengambil pemiliknya, lewat reference: penunjuk ke nilai milik variabel lain, ditulis dengan &. Bayangin nota Sari di Kedai Martabak Bang Rusdi. Di pelajaran ownership, kasir menyerahkan notanya ke dapur, dan kasir gak memegangnya lagi. Padahal dapur sering cuma perlu membaca. Cukup kasir tunjukkan notanya: dapur membaca, nota tetap di tangan kasir. Kasir, dapur, dan Sari boleh membaca nota yang sama bersamaan. Tapi kalau ada yang sedang menulis tambahan di nota, gak boleh ada orang lain yang membaca atau menulis saat itu, karena bisa salah baca tulisan yang belum selesai. Menunjukkan nota itu &nota, meminjamkan pena itu &mut nota, dan aturan "banyak yang membaca, atau satu yang menulis" itu aturan borrowing.

🧪 Tebakan cepat: kasir mencatat pesanan pertama lewat pinjaman, let pertama = &pesanan[0];, lalu ada pesanan baru, pesanan.push(45000);, lalu kasir mencetak pertama. Lolos compile? Tebak dulu, jawabannya ada di bagian "Kenapa aturannya seketat itu".

&: meminjam untuk membaca

fn hitung_panjang(s: &String) -> usize {
    s.len()
}

fn masak(nota: &String) {
    println!("Dimasak: {}", nota);
}

fn main() {
    let nota = String::from("2x martabak manis");
    let panjang = hitung_panjang(&nota);
    masak(&nota);
    println!("Kasir: {} ({} huruf)", nota, panjang);
}
Dimasak: 2x martabak manis
Kasir: 2x martabak manis (17 huruf)

&mut: meminjam untuk mengubah

fn tambah_item(nota: &mut String, item: &str) {
    nota.push_str(item);
}

fn tambah_es_teh(total: &mut u32) {
    *total += 5000;
}

fn main() {
    let mut nota = String::from("2x martabak manis");
    let mut total: u32 = 102000;
    tambah_item(&mut nota, ", 1x es teh");
    tambah_es_teh(&mut total);
    println!("{}", nota);
    println!("Total: {}", total);
}
2x martabak manis, 1x es teh
Total: 107000

Aturannya: banyak pembaca, atau satu penulis

Pada satu waktu, sebuah nilai boleh punya salah satu dari ini:

  1. Pinjaman & sebanyak apa pun, atau
  2. Tepat satu pinjaman &mut, dan selama ia hidup gak ada pinjaman lain.

Jadi &mut cuma boleh satu pada satu waktu. Dua penulis sekaligus ditolak:

fn main() {
    let mut nota = String::from("2x martabak manis");
    let kasir = &mut nota;
    let dapur = &mut nota;
    kasir.push_str(", 1x es teh");
    dapur.push_str(", dibungkus");
    println!("{}", nota);
}
error[E0499]: cannot borrow `nota` as mutable more than once at a time
 --> src/main.rs:4:17
  |
3 |     let kasir = &mut nota;
  |                 --------- first mutable borrow occurs here
4 |     let dapur = &mut nota;
  |                 ^^^^^^^^^ second mutable borrow occurs here
5 |     kasir.push_str(", 1x es teh");
  |     ----- first borrow later used here

Pemiliknya sendiri juga ikut aturan. Selama ada yang meminjam, pemilik gak boleh mengubah nilainya, bahkan angka u32: let kasir = &harga; harga = 35000; lalu memakai kasir ditolak dengan E0506 cannot assign to `harga` because it is borrowed.

Kenapa aturannya seketat itu

Ini kode tebakan tadi:

fn main() {
    let mut pesanan = vec![102000, 70000, 38000];
    let pertama = &pesanan[0];
    pesanan.push(45000);
    println!("Pertama: {}", pertama);
}
error[E0502]: cannot borrow `pesanan` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let pertama = &pesanan[0];
  |                    ------- immutable borrow occurs here
4 |     pesanan.push(45000);
  |     ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("Pertama: {}", pertama);
  |                             ------- immutable borrow later used here

Gak lolos. Itu jawaban tebakannya. push butuh &mut pesanan, padahal pertama masih meminjam untuk membaca. Ini bukan aturan iseng. vec![...] berisi 3 angka punya kapasitas pas 3, jadi push memesan blok heap baru berkapasitas 6, memindahkan isinya ke sana, lalu mengembalikan blok lama. Saat dicoba, alamat isinya berubah dari 0x100dbded0 ke 0x977000980. Kalau compiler membiarkannya, pertama akan menunjuk blok lama yang sudah dikembalikan: dangling reference, bug yang di C++ lolos compile lalu membaca sampah atau crash. Aturan "satu penulis" juga yang membuat program dengan banyak thread aman dari data race: dua thread menulis data yang sama bersamaan.

Pinjaman berakhir di pemakaian terakhir

Pindahkan println! ke atas push, dan programnya lolos:

fn main() {
    let mut pesanan = vec![102000, 70000, 38000];
    let pertama = &pesanan[0];
    println!("Pertama: {}", pertama);
    pesanan.push(45000);
    println!("{:?}", pesanan);
}
Pertama: 102000
[102000, 70000, 38000, 45000]

Pinjaman pertama gak hidup sampai }, tapi cuma sampai pemakaian terakhirnya, yaitu baris println! pertama. Sesudah itu, push boleh meminjam untuk menulis. Fitur ini namanya non-lexical lifetimes (NLL), ada sejak edition 2018. Jadi kalau ketemu E0502 atau E0499, sering perbaikannya cukup memindahkan pemakaian pinjaman lama ke atas pinjaman baru.

Referensi gak boleh hidup lebih lama dari datanya

fn main() {
    let catatan;
    {
        let nota = String::from("2x martabak manis");
        catatan = &nota;
    }
    println!("{}", catatan);
}
error[E0597]: `nota` does not live long enough
 --> src/main.rs:5:19
  |
4 |         let nota = String::from("2x martabak manis");
  |             ---- binding `nota` declared here
5 |         catatan = &nota;
  |                   ^^^^^ borrowed value does not live long enough
6 |     }
  |     - `nota` dropped here while still borrowed
7 |     println!("{}", catatan);
  |                    ------- borrow later used here

nota di-drop di } blok dalam, tapi catatan masih dipakai sesudahnya. Rentang hidup sebuah pinjaman disebut lifetime, dan compiler memastikan tiap lifetime selesai sebelum datanya dibuang.

Fungsi yang mengembalikan reference kadang perlu menyebut lifetime-nya sendiri. Fungsi ini memilih teks yang lebih panjang:

fn terpanjang<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    println!("{}", terpanjang("martabak manis", "es teh"));
}

Output-nya martabak manis. Tanpa <'a>, fn terpanjang(a: &str, b: &str) -> &str ditolak dengan E0106 missing lifetime specifier, karena compiler gak tahu hasilnya meminjam dari a atau dari b. 'a cuma nama untuk sebuah lifetime: tulisan di atas berjanji "hasilnya boleh dipakai selama a dan b dua-duanya masih hidup". Lifetime gak memperpanjang umur apa pun, ia cuma menjelaskan. Fungsi yang membuat String sendiri lalu mengembalikan &String juga kena E0106, dan saran compiler-nya tepat: instead, you are more likely to want to return an owned value, yaitu kembalikan String-nya.

Slice: meminjam sebagian

fn total(item: &[u32]) -> u32 {
    item.iter().sum()
}

fn main() {
    let nota = String::from("2x martabak manis");
    let porsi: &str = &nota[0..2];
    let menu: &str = &nota[3..];
    let pesanan = vec![102000, 70000, 38000];
    let papan: [u32; 4] = [32000, 38000, 5000, 5000];
    println!("{} | {}", porsi, menu);
    println!("{:?}", &pesanan[1..]);
    println!("{}", total(&pesanan));
    println!("{}", total(&pesanan[1..]));
    println!("{}", total(&papan));
}
2x | martabak manis
[70000, 38000]
210000
108000
80000

⚠️ Jebakan yang sering ketemu

TL;DR: &nota meminjam untuk membaca, &mut nota meminjam untuk mengubah, dan pemiliknya tetap nota, jadi gak ada move dan gak ada drop saat pinjaman selesai. Pada satu waktu boleh banyak & atau tepat satu &mut (E0502, E0499), dan selama dipinjam pemiliknya pun gak boleh mengubah (E0506). Aturan ini mencegah dangling reference, misalnya pinjaman ke isi Vec yang pindah saat push. Pinjaman berakhir di pemakaian terakhirnya (NLL), bukan di }. Reference gak boleh hidup lebih lama dari datanya (E0597), dan fungsi yang mengembalikan reference dari dua parameter menulis lifetime-nya: fn terpanjang<'a>(a: &'a str, b: &'a str) -> &'a str. Slice (&str, &[u32]) meminjam sebagian isi, 16 byte, dan jadi tipe parameter yang paling luwes.

Yang akan kamu pelajari