Referensi dan Borrowing

Operator & dan &mut serta pemisahan akses baca dan tulis.

Daftar isi

Topik ini membahas borrowing, yaitu mekanisme memakai nilai melalui referensi tanpa mengambil alih kepemilikannya. Pembahasan mencakup operator & untuk pinjaman baca, operator &mut untuk pinjaman tulis, pemisahan kedua mode akses tersebut, serta jaminan bahwa referensi menggantung (dangling reference) tertolak saat kompilasi. Setelah membaca topik ini, fungsi dapat ditulis agar meminjam data pemanggilnya alih-alih menghabiskan kepemilikan data itu, dan pesan error yang berkenaan dengan referensi dapat dikenali penyebabnya.

Referensi sebagai pinjaman#

Referensi adalah nilai yang menunjuk ke data milik variabel lain tanpa memiliki data tersebut, dan pemakaian referensi disebut borrowing. Sintaks pembuatannya adalah operator & yang diletakkan sebelum nama variabel, sedangkan tipenya ditulis dengan & yang dilekatkan pada tipe data yang ditunjuk, misalnya &String. Referensi bersifat seperti penunjuk yang sudah diperiksa kompilator: selama referensi hidup, data yang ditunjuknya dijamin masih valid, sehingga program tidak dapat mengakses memori yang sudah dibebaskan.

Kepemilikan data tetap berada pada variabel asal, dan referensi hanya memegang izin akses. Diagram berikut memperlihatkan hubungan antara pemilik catatan bertipe String, isi teksnya di heap, serta referensi r yang meminjam struktur kendalinya.

r: &catatancatatan: ptr | len | cap(String, pemilik)"suhu 30"meminjamptrr hanya membaca, catatan tetap pemilik
Referensi meminjam nilai tanpa mengambil alih kepemilikan data di heap

Referensi baca tidak dapat mengubah data yang ditunjuknya, sebagaimana variabel biasa yang secara bawaan juga tidak dapat diubah tanpa mut. Untuk memperoleh izin mengubah, pemanggil harus menyatakan pinjaman tulis secara eksplisit dengan &mut, dan variabel asalnya harus dideklarasikan dengan let mut. Pemisahan dua mode ini memungkinkan kompilator memastikan bahwa perubahan data tidak terjadi di tengah pembacaan yang sedang berjalan.

Fungsi yang meminjam alih-alih memiliki#

Fungsi yang menerima String sebagai parameter menghabiskan kepemilikan nilai tersebut, sehingga pemanggil harus menerima kembali nilainya apabila masih ingin memakainya. Tanda tangan yang meminjam cukup memakai &String, dan pemanggil membuat referensi dengan &s1 pada titik pemanggilan. Setelah fungsi selesai, pinjaman berakhir dan variabel asal tetap berlaku penuh sebagai pemilik.

rust pinjam-baca.rs
fn hitung_panjang(s: &String) -> usize {
    s.len()
}

fn main() {
    let s1 = String::from("kartu-pintar");
    let panjang = hitung_panjang(&s1); // s1 dipinjam, bukan diserahkan

    println!("Panjang '{}' = {}", s1, panjang);
    println!("s1 masih bisa dipakai: {}", s1);
}
text Keluaran
Panjang 'kartu-pintar' = 12
s1 masih bisa dipakai: kartu-pintar

Keluaran tersebut menunjukkan s1 tercetak dua kali setelah dipinjam oleh fungsi hitung_panjang. Pinjaman hanya hidup selama pemanggilan fungsi berlangsung, karena parameter s berakhir saat fungsi kembali. Bandingkan dengan versi yang menerima String biasa: kepemilikan berpindah ke parameter, dan pemakaian s1 pada baris setelahnya ditolak dengan error E0382. Selama pinjaman hidup, fungsi hanya dapat membaca datanya karena referensinya tidak menyandang mut, sehingga pemanggil mendapat jaminan bahwa nilainya tidak berubah oleh pemanggilan itu. Dengan tanda tangan semacam ini, fungsi dapat dipanggil berulang kali atas variabel yang sama tanpa satu pun pemanggilan merusak pemanggilan lainnya.

Referensi tulis dengan &mut#

Pinjaman tulis dibuat dengan &mut pada titik pemanggilan dan &mut String pada tanda tangan fungsinya. Melalui referensi semacam ini, fungsi dapat mengubah isi data yang dipinjamnya, dan hasil perubahan tetap terlihat pada variabel pemiliknya. Referensi tulis meminta dua hal dari kode pemanggil: variabel asal dideklarasikan let mut, dan tidak ada pinjaman lain yang hidup bersamaan pada saat &mut dibuat.

rust pinjam-tulis.rs
fn bubuhkan_awalan(teks: &mut String) {
    teks.push_str(" [terverifikasi]");
}

fn main() {
    let mut catatan = String::from("suhu 30");
    bubuhkan_awalan(&mut catatan); // dipinjam sebagai akses tulis

    println!("catatan = {}", catatan);
}
text Keluaran
catatan = suhu 30 [terverifikasi]

Program tersebut mengubah isi catatan dari dalam fungsi tanpa memindahkan kepemilikannya. Ketika bubuhkan_awalan selesai, pinjaman tulis berakhir dan pemanggil kembali memegang kendali penuh atas variabelnya. Fungsi yang sama dapat dipanggil lagi setelahnya, dan setiap pemanggilan memperoleh pinjamannya bergantian, bukan bersamaan. Pembatasan jumlah pinjaman tulis yang hidup bersamaan, beserta alasannya, dibahas secara khusus pada topik Aturan Borrowing.

Referensi menggantung dicegah saat kompilasi#

Referensi menggantung adalah referensi yang masih hidup setelah data yang ditunjuknya dibebaskan, dan akses lewat referensi seperti itu pada banyak bahasa merupakan sumber bug yang sulit dilacak. Rust menolak pembuatan referensi menggantung sejak kompilasi dengan aturan sederhana: sebuah referensi tidak boleh berumur lebih panjang dari data yang ditunjuknya. Bentuk penolakannya dapat diperiksa langsung pada fungsi berikut, yang mencoba mengembalikan referensi ke String lokal miliknya sendiri.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: referensi menggantung
printf 'fn gantung() -> &String {\n    let s = String::from("sementara");\n    &s\n}\n\nfn main() {\n    gantung();\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,7p'
text Keluaran
error[E0106]: missing lifetime specifier
 --> salah.rs:1:17
  |
1 | fn gantung() -> &String {
  |                 ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Baris kunci pada keluaran tersebut adalah this function's return type contains a borrowed value, but there is no value for it to be borrowed from. Kompilator menolak nilai kembalian bertipe &String karena tidak ada data yang dapat dipinjam secara sah: variabel s dibuat di dalam fungsi dan dijatuhkan tepat saat fungsi selesai, sehingga referensi ke s pasti menggantung pada saat dipakai pemanggil. Perbaikan yang benar adalah mengembalikan kepemilikan sepenuhnya dengan tipe kembalian String, karena nilai yang dimiliki dapat berpindah keluar dari fungsi dengan aman. Pembahasan tentang cara menuliskan hubungan umur antar referensi ada pada topik Lifetime Putaran Pertama.

Kesalahan umum#

Kesalahan pertama adalah menyerahkan kepemilikan padahal fungsi hanya membaca. Tanda tangan fn proses(teks: String) memaksa pemanggil kehilangan nilainya, dan pemanggil harus menerimanya kembali atau mengkloningnya hanya demi membaca panjang teks. Perbaikannya adalah mengganti parameter menjadi &String atau &str dan menyerahkan referensi pada titik pemanggilan, sehingga fungsi hanya meminjam data selama dibutuhkan. Petunjuk praktisnya, fungsi yang tidak mengubah dan tidak menyimpan parameternya hampir selalu cukup meminjam.

Kesalahan kedua adalah mencampur pinjaman baca dan tulis pada saat bersamaan, misalnya membuat &mut ketika referensi baca masih akan dipakai setelahnya. Kompilator menolak pola tersebut dengan error E0502 dan menunjukkan titik pinjaman baca, titik pinjaman tulis, serta titik pemakaian terakhirnya. Urutkan kodenya sehingga seluruh pemakaian pinjaman lama selesai sebelum pinjaman tulis dibuat, atau pisahkan keduanya ke dalam blok yang berbeda apabila urutan logikanya memang tidak dapat diubah. Tabrakan seperti ini sering muncul pada perulangan yang membaca dan menulis variabel yang sama secara bergantian, sehingga solusinya berupa pemisahan fase baca dan fase tulis.

Ringkasan#

  • Referensi dibuat dengan & untuk pinjaman baca dan &mut untuk pinjaman tulis, sementara kepemilikan tetap pada variabel asalnya.
  • Tanda tangan &String membuat fungsi meminjam data, sehingga pemanggil tetap dapat memakai variabelnya setelah pemanggilan selesai.
  • Pinjaman tulis mengubah data pemiliknya dan menuntut deklarasi let mut pada variabel asal.
  • Referensi menggantung tidak dapat lolos kompilasi: referensi yang berumur lebih panjang dari datanya ditolak, contohnya lewat error E0106 pada fungsi yang mengembalikan &String lokal.

Referensi#