Aturan Borrowing

Aturan satu penulis atau banyak pembaca dan logika borrow checker.

Daftar isi

Topik ini membahas aturan borrowing yang membatasi jumlah pinjaman baca dan pinjaman tulis atas satu nilai, beserta cara borrow checker menegakkannya saat kompilasi. Pembahasan mencakup makna “hidup bersamaan” bagi sebuah pinjaman, dua pelanggaran yang paling sering dijumpai beserta pesan errornya, dan alasan di balik aturan tersebut. Setelah membaca topik ini, konflik pinjaman dapat dilokalisasi dari pesan errornya, dan kode dapat disusun ulang agar pembacaan dan penulisan data tidak bertabrakan.

Aturan satu penulis atau banyak pembaca#

Aturan borrowing pada Rust berbunyi ringkas: pada satu waktu, sebuah nilai hanya boleh dipinjam oleh satu referensi tulis &mut, atau oleh sejumlah referensi baca & sekaligus, tetapi tidak oleh campuran keduanya. Aturan ini adalah kelanjutan langsung dari pemisahan mode akses pada referensi, karena referensi baca menjanjikan data tidak berubah selama pinjaman hidup, sedangkan referensi tulis meminta kebebasan penuh untuk mengubah. Kedua janji tersebut tidak dapat dipenuhi bersamaan, sehingga salah satunya harus menunggu.

Diagram berikut merangkum kedua keadaan yang diperbolehkan. Pada panel kiri, tiga referensi baca mengarah ke data yang sama karena tidak ada satu pun yang mengubah. Pada panel kanan, hanya satu referensi tulis yang diakui, dan referensi tulis kedua ditolak karena penulis lain masih ada.

Banyak pembacar1 &r2 &r3 &dataSatu penulisr1 &mutr2 &mutdataditolak
Aturan borrowing: banyak pembaca atau satu penulis, tidak pernah campuran

Lingkup pinjaman berakhir di pemakaian terakhirnya#

Masa hidup sebuah pinjaman berawal saat referensinya dibuat dan berakhir pada pemakaian terakhirnya, bukan pada kurung kurawal penutup bloknya. Perilaku ini disebut non-lexical lifetime karena batas pinjaman tidak lagi terikat pada blok leksikal. Konsekuensinya, pinjaman tulis dapat dibuat setelah seluruh pemakaian pinjaman baca selesai, meskipun pinjaman baca itu dideklarasikan dalam blok yang sama. Pemakaian yang dihitung mencakup pembacaan nilai, pemanggilan metode, hingga penyerahan referensi ke fungsi lain, sehingga kompilator harus melihat seluruh titik pemakaian itu sebelum dapat menentukan batas akhir pinjaman.

Aturan ini juga berlaku ke arah sebaliknya. Pinjaman tulis yang masih akan dipakai di baris terakhir sebuah blok tetap hidup sampai baris itu, sehingga pinjaman baca tidak dapat diselipkan di tengah masa hidupnya. Dengan kata lain, yang dibandingkan adalah rentang pemakaian nyata dari masing-masing referensi, dan bukan sekadar posisi deklarasinya di dalam kode.

rust dua-pembaca.rs
fn main() {
    let mut data = String::from("tekanan");

    let r1 = &data;
    let r2 = &data; // banyak pembaca dipersilakan
    println!("r1 = {}, r2 = {}", r1, r2);

    // pinjaman baca r1 dan r2 berakhir di pemakaian terakhirnya di atas,
    // sehingga pinjaman tulis di bawah ini sah
    let r3 = &mut data;
    r3.push_str("-final");
    println!("data = {}", data);
}
text Keluaran
r1 = tekanan, r2 = tekanan
data = tekanan-final

Program tersebut sah karena r1 dan r2 terakhir dipakai pada pemanggilan println! pertama, sehingga pinjaman bacanya berakhir di titik itu. Pinjaman tulis r3 baru dibuat setelahnya, dan kedua masa pinjaman tidak beririsan. Apabila baris println! pertama dipindahkan ke bawah pembuatan r3, pinjaman baca dan tulis akan hidup bersamaan dan kompilasi gagal, seperti diperlihatkan pada bagian berikutnya.

Pelanggaran: pinjaman baca dan tulis bersamaan#

Campuran pinjaman baca dan tulis dalam masa yang sama ditolak dengan error E0502. Program berikut membuat r1 sebagai pembaca, membuat r2 sebagai penulis, lalu masih memakai keduanya pada satu pemanggilan println!. Susunan seperti ini terasa wajar ketika ditulis, karena deklarasi r1 dan r2 berdampingan, tetapi masa pinjaman keduanya saling menutupi hingga baris kelima.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: pinjaman baca dan tulis hidup bersamaan
printf 'fn main() {\n    let mut data = String::from("tekanan");\n    let r1 = &data;\n    let r2 = &mut data;\n    println!("{}, {}", r1, r2);\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,9p'
text Keluaran
error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
 --> salah.rs:4:14
  |
3 |     let r1 = &data;
  |              ----- immutable borrow occurs here
4 |     let r2 = &mut data;
  |              ^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}, {}", r1, r2);
  |                        -- immutable borrow later used here

Tiga baris kunci pada pesan tersebut menceritakan kronologi pelanggarannya. Baris immutable borrow occurs here menandai pembuatan pinjaman baca r1, baris mutable borrow occurs here menandai pembuatan pinjaman tulis r2, dan baris immutable borrow later used here menandai pemakaian r1 yang masih terjadi setelahnya. Tanpa pemakaian terakhir tersebut, pinjaman baca sudah berakhir dan program justru sah, sebagaimana pada contoh dua-pembaca.rs. Bahaya pola yang ditolak ini konkret: push_str pada r2 dapat memicu realokasi penyangga heap, sehingga r1 berakhir menunjuk memori yang sudah dikembalikan. Perbaikannya tidak menuntut restrukturisasi besar: pindahkan pemakaian r1 ke sebelum pembuatan r2, atau bungkus masing-masing pinjaman dalam blok tersendiri apabila keduanya memang harus membagi satu variabel.

Pelanggaran: dua penulis sekaligus#

Dua pinjaman tulis dalam masa yang sama juga ditolak, kali ini dengan error E0499. Program berikut hanya mengubah r1 dari referensi baca menjadi referensi tulis.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: dua pinjaman tulis hidup bersamaan
printf 'fn main() {\n    let mut data = String::from("tekanan");\n    let r1 = &mut data;\n    let r2 = &mut data;\n    println!("{}, {}", r1, r2);\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,9p'
text Keluaran
error[E0499]: cannot borrow `data` as mutable more than once at a time
 --> salah.rs:4:14
  |
3 |     let r1 = &mut data;
  |              --------- first mutable borrow occurs here
4 |     let r2 = &mut data;
  |              ^^^^^^^^^ second mutable borrow occurs here
5 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

Struktur pesannya sejajar dengan E0502: pembuatan pinjaman tulis pertama, pembuatan pinjaman tulis kedua, lalu pemakaian pinjaman pertama yang memperpanjang masa hidupnya melewati pinjaman kedua. Dua penulis pada satu data dapat menimbulkan data race pada program paralel, dan pada program sekuensial pun hasilnya tidak dapat dipastikan karena urutan dua penulisan yang bertindihan tidak terdefinisi. Dengan menolak pola ini sejak kompilasi, Rust memindahkan seluruh kelas masalah tersebut dari sesi penelusuran kesalahan ke pesan error yang menunjuk baris penyebabnya.

Situasi dua penulis biasanya lahir dari dua niat yang sama-sama sah, misalnya satu bagian kode mengisi data dan bagian lain memformatnya. Kedua niat itu tetap harus mengantre: seluruh pemakaian penulis pertama diselesaikan dahulu, baru penulis kedua dibuat. Dengan begitu setiap titik kode hanya melihat satu keadaan data, dan pembacaan tidak pernah menangkap hasil penulisan yang setengah jadi.

Mengapa aturan ini ada#

Aturan satu penulis atau banyak pembaca mencegah pola bug yang dikenal luas dengan nama aliasing disertai mutasi, yaitu keadaan ketika beberapa penunjuk mengarah ke data yang sama dan salah satunya mengubah data itu di tengah pembacaan. Kasus klasiknya adalah referensi ke elemen pertama sebuah Vec yang menjadi tidak berarti karena penambahan elemen memicu realokasi, atau dua bagian program yang saling menimpa hasil tulisannya. Bahasa tanpa pemeriksaan semacam ini menyerahkan persoalan kepada disiplin programer atau kepada alat runtime.

Borrow checker adalah pemeriksa statis yang mencatat pembuatan dan pemakaian setiap referensi, lalu memastikan tidak ada dua akses yang bertentangan dalam masa yang beririsan. Pemeriksaan ini hanya berjalan pada saat kompilasi, sehingga program hasil kompilasi tidak membawa biaya pemantauan apa pun untuk aturan tersebut. Manfaatnya menjalar sampai ke program paralel: dua utas yang memegang referensi terhadap data yang sama tidak akan pernah membentuk kombinasi tulis-baca atau tulis-tulis, sehingga data race tertolak sebelum program berjalan.

Kesalahan umum#

Kesalahan pertama adalah menganggap masa pinjaman berakhir di kurung kurawal penutup, sehingga aturan terasa lebih ketat dari kenyataannya. Pinjaman sesungguhnya berakhir di pemakaian terakhirnya, sehingga memindahkan satu baris pemakaian sering kali sudah cukup untuk meloloskan kode. Sebelum merestrukturisasi fungsi, periksa dahulu apakah pemakaian terakhir pinjaman baca dapat diselesaikan sebelum pinjaman tulis dibuat. Pola yang sering menjebak adalah mengisi Vec sambil memegang referensi ke elemen pertamanya, karena operasi push dapat memicu realokasi dan membatalkan seluruh referensi lama. Kumpulkan dahulu nilai yang dibutuhkan pinjaman baca, lalu lakukan penulisan setelah pinjaman itu selesai.

Kesalahan kedua adalah menganggap pesan error menunjuk baris yang salah. Baris bertanda mutable borrow occurs here memang menunjuk pembuatan referensi tulisnya, tetapi konfliknya baru terjadi karena baris borrow later used here yang memperpanjang masa pinjaman sebelumnya. Perbaikan yang tepat biasanya jatuh pada salah satu dari dua titik tersebut: akhiri pemakaian pinjaman lama lebih awal, atau tunda pembuatan pinjaman baru hingga pemakaian lama benar-benar selesai.

Ringkasan#

  • Pada satu waktu, satu nilai hanya boleh dipinjam satu referensi tulis &mut atau sejumlah referensi baca &, tidak pernah campuran keduanya.
  • Masa pinjaman berawal dari pembuatan referensi sampai pemakaian terakhirnya, sehingga urutan pemakaian menentukan sah tidaknya kombinasi pinjaman.
  • Campuran baca-tulis ditolak dengan error E0502 dan dua penulis dengan error E0499, keduanya lengkap dengan titik pembuatan dan titik pemakaian terakhir.
  • Aturan ini mencegah aliasing disertai mutasi serta data race, dan penegakannya tidak menimbulkan biaya runtime.

Referensi#