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:
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.
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!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
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:
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:
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:
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:
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.