Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Referensi dan Borrowing

Masalah dari kode tuple di Listing 4-5 adalah kita harus balikin String-nya ke fungsi pemanggil biar kita tetep bisa pake String-nya setelah manggil calculate_length, soalnya String-nya udah di-move ke dalem calculate_length. Sebagai gantinya, kita bisa ngasih sebuah referensi ke nilai String itu. Sebuah reference (referensi) itu kayak pointer karena dia adalah alamat yang bisa kita ikutin buat akses data yang disimpan di alamat itu; data itu dimiliki sama variabel lain. Beda sama pointer, sebuah referensi dijamin bakal nunjuk ke sebuah nilai yang valid dari tipe tertentu selama masa hidup referensi itu.

Ini cara kita mendefinisikan dan pake fungsi calculate_length yang punya referensi ke sebuah objek sebagai parameter bukannya ngambil ownership dari nilainya:

Filename: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

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

Pertama, perhatiin kalau semua kode tuple di deklarasi variabel sama nilai return fungsi udah nggak ada. Kedua, perhatiin kalau kita masukin &s1 ke calculate_length dan, di definisinya, kita nerima &String bukannya String. Tanda ampersand ini merepresentasikan references, dan mereka ngebolehin kita buat ngerujuk ke suatu nilai tanpa ngambil ownership-nya. Gambar 4-6 ngeliatin konsep ini.

Tiga tabel: tabel buat s isinya cuma pointer ke tabel buat s1. Tabel 
buat s1 isinya data stack buat s1 dan nunjuk ke data string di heap.

Gambar 4-6: Diagram &String s yang nunjuk ke String s1

Catatan: Kebalikan dari bikin referensi pake & adalah dereferencing, yang dilakuin pake operator dereference, *. Kita bakal liat beberapa penggunaan operator dereference di Bab 8 dan bahas detail soal dereferencing di Bab 15.

Yuk kita liat lebih deket pemanggilan fungsinya di sini:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

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

Sintaks &s1 ngebolehin kita bikin sebuah referensi yang ngerujuk ke nilai dari s1 tapi nggak memilikinya. Karena referensinya nggak memiliki nilainya, nilai yang dia tunjuk nggak bakal di-drop pas referensinya udah nggak dipake lagi.

Begitu juga sama signature fungsinya yang pake & buat nunjukin kalau tipe parameternya s itu adalah sebuah referensi. Yuk kita tambahin beberapa anotasi penjelasan:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

Scope di mana variabel s itu valid sama kayak scope parameter fungsi mana pun, tapi nilai yang ditunjuk sama referensinya nggak bakal di-drop pas s udah nggak dipake lagi, karena s nggak punya ownership. Pas fungsi punya referensi sebagai parameter bukannya nilai aslinya, kita nggak perlu balikin nilai-nilainya buat ngasih balik ownership, karena emang kita nggak pernah punya ownership-nya dari awal.

Kita sebut aksi bikin referensi ini sebagai borrowing (meminjam). Kayak di dunia nyata, kalau seseorang punya sesuatu, kita bisa pinjem dari mereka. Pas udah selese, kita harus balikin. Kita nggak memilikinya.

Jadi, apa yang terjadi kalau kita nyoba ngerubah sesuatu yang kita pinjem? Coba kode di Listing 4-6. Spoiler: kodenya nggak bakal jalan!

Filename: src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
Listing 4-6: Nyoba ngerubah nilai yang dipinjem

Ini error-nya:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Sama kayak variabel yang immutable secara default, referensi juga gitu. Kita nggak dibolehin ngerubah sesuatu yang kita punya referensinya.

Mutable References

Kita bisa benerin kode dari Listing 4-6 biar kita dibolehin ngerubah nilai yang dipinjem dengan cuma beberapa perubahan kecil yang pake mutable reference sebagai gantinya:

Filename: src/main.rs
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Pertama kita ubah s jadi mut. Terus kita bikin sebuah mutable reference pake &mut s pas kita manggil fungsi change, terus update signature fungsinya biar nerima sebuah mutable reference pake some_string: &mut String. Ini bikin keliatan jelas sekali kalau fungsi change bakal ngerubah (mutate) nilai yang dia pinjem.

Mutable references punya satu larangan gede: kalau kita punya sebuah mutable reference ke sebuah nilai, kita nggak boleh punya referensi lain ke nilai itu. Kode ini yang nyoba bikin dua mutable reference ke s bakal gagal:

Filename: src/main.rs
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}

Ini error-nya:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Error ini bilang kalau kodenya nggak valid karena kita nggak bisa minjem s sebagai mutable lebih dari sekali dalam satu waktu. Mutable borrow yang pertama ada di r1 dan harus bertahan sampe dia dipake di println!, tapi di antara pembuatan mutable reference itu sampe penggunaannya, kita nyoba bikin mutable reference lain di r2 yang minjem data yang sama kayak r1.

Larangan yang nyegah banyak mutable reference ke data yang sama di waktu yang bersamaan ini ngebolehin adanya mutasi tapi dengan cara yang sangat terkontrol. Ini hal yang biasanya bikin Rustacean baru rada pusing karena kebanyakan bahasa ngebolehin kita ngerubah nilai kapan pun kita mau. Keuntungan punya larangan ini adalah Rust bisa nyegah data races pas compile time. Sebuah data race itu mirip kayak race condition dan terjadi pas tiga perilaku ini muncul:

  • Dua atau lebih pointer akses data yang sama di waktu yang sama.
  • Minimal salah satu dari pointer-nya dipake buat nulis ke datanya.
  • Nggak ada mekanisme yang dipake buat sinkronisasi akses ke datanya.

Data races bikin perilaku yang nggak terdefinisi (undefined behavior) dan bisa susah buat didiagnosa dan diperbaiki pas kita nyoba nyari tau pas runtime; Rust nyegah masalah ini dengan nolak buat nge-compile kode yang punya data races!

Kayak biasa, kita bisa pake kurung kurawal buat bikin scope baru, yang ngebolehin adanya banyak mutable reference, cuma bukan yang bersamaan:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Rust juga nerapin aturan yang mirip buat ngelempokin mutable sama immutable references. Kode ini ngasilin error:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{r1}, {r2}, and {r3}");
}

Ini error-nya:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Fiuh! Kita juga nggak bisa punya sebuah mutable reference pas kita lagi punya sebuah immutable reference ke nilai yang sama.

User dari sebuah immutable reference nggak bakal nyangka kalau nilainya tiba- tiba berubah gitu aja! Tapi, banyak immutable references diperbolehkan karena nggak ada orang yang cuma baca datanya punya kemampuan buat ngaruhin bacaan data orang lain.

Perhatiin ya kalau scope sebuah referensi dimulai dari tempat dia dikenalin sampe terakhir kali referensi itu dipake. Misalnya, kode ini bakal ke-compile karena penggunaan terakhir dari immutable references ada di println!, sebelum mutable reference-nya dikenalin:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

Scope dari immutable references r1 sama r2 abis setelah println! di mana mereka terakhir dipake, yang mana itu sebelum mutable reference r3 dibuat. Scope-scope ini nggak tumpang tindih, jadi kode ini diperbolehkan: compiler bisa tau kalau referensinya udah nggak dipake lagi di titik sebelum akhir dari scope-nya.

Walaupun error borrowing kadang bikin kesel, inget ya kalau itu adalah compiler Rust yang lagi nunjukin potensi bug dari awal (pas compile time bukannya pas runtime) dan nunjukin tepat di mana letak masalahnya. Jadi kita nggak perlu repot-repot nyari tau kenapa data kita nggak sesuai sama apa yang kita pikirkan.

Dangling References

Di bahasa yang punya pointer, gampang sekali buat nggak sengaja bikin sebuah dangling pointer—sebuah pointer yang ngerujuk ke sebuah lokasi di memori yang mungkin udah dikasih ke orang lain—dengan cara ngebebasin sejumlah memori tapi tetep nyimpen pointer ke memori itu. Di Rust, sebaliknya, compiler ngejamin kalau referensi nggak bakal pernah jadi dangling references: kalau kita punya sebuah referensi ke suatu data, compiler bakal mastiin kalau datanya nggak bakal keluar dari scope sebelum referensi ke datanya keluar duluan.

Yuk kita coba bikin sebuah dangling reference buat liat gimana Rust nyegah mereka pake compile-time error:

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Ini error-nya:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &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
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Pesan error ini ngerujuk ke fitur yang belum kita bahas: lifetimes. Kita bakal bahas lifetimes secara detail di Bab 10. Tapi, kalau kita cuekin bagian soal lifetimes-nya, pesannya emang isinya kunci kenapa kode ini bermasalah:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Yuk kita liat lebih deket apa sebenernya yang terjadi di tiap tahap kode dangle kita:

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
  // Danger!

Karena s dibuat di dalem dangle, pas kode dangle selesai, s bakal di-dealokasi. Tapi kita nyoba buat balikin sebuah referensi kepadanya. Itu artinya referensi ini bakal nunjuk ke sebuah String yang nggak valid. Itu nggak oke sekali! Rust nggak bakal ngebolehin kita ngelakuin ini.

Solusinya di sini adalah dengan balikin String-nya secara langsung:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Ini jalan tanpa masalah apa pun. Ownership di-move keluar, dan nggak ada apa pun yang di-dealokasi.

Aturan Referensi

Yuk kita ringkas apa yang udah kita bahas soal referensi:

  • Dalam satu waktu, kita bisa punya antara satu mutable reference atau sejumlah berapa pun immutable references.
  • Referensi harus selalu valid.

Selanjutnya, kita bakal liat jenis referensi yang beda: slices.