Lewati ke konten

Lifetime di Rust

Lifetime (masa hidup sebuah referensi) biasanya menjadi titik di mana developer Rust pemula pertama kali benar-benar “mentok”. Kalau kamu merasa begitu, tenang, kamu tidak sendirian. Halaman ini sengaja dibuat santai: tujuannya hanya supaya kamu bisa mengenali apa itu lifetime dan kenapa compiler memintanya, bukan untuk menguasai semua aturannya.

Dari halaman borrowing kamu sudah tahu bahwa referensi (&) membuatmu bisa memakai sebuah nilai tanpa memilikinya. Tapi referensi hanya valid selama nilai yang ditunjuknya masih ada. Rust harus memastikan kamu tidak pernah memegang referensi ke sesuatu yang sudah dibersihkan. Bug semacam itu disebut dangling reference (referensi menggantung), dan merupakan penyebab crash klasik di bahasa seperti C.

Hampir selalu, Rust bisa memeriksanya sendiri dengan melihat scope di kodemu, jadi kamu tidak perlu memikirkannya sama sekali (persis seperti yang kamu lihat di halaman Scope). Tapi kadang, terutama pada fungsi, compiler tidak bisa menyimpulkannya sendiri dan memintamu menuliskannya secara eksplisit. Itulah yang disebut anotasi lifetime.

Ini fungsi yang mengembalikan string slice yang lebih panjang di antara dua string slice:

fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}

Kode ini tidak bisa dikompilasi. Error-nya kira-kira seperti ini:

error[E0106]: missing lifetime specifier

Artinya ada penanda lifetime yang kurang. Masalahnya: referensi yang dikembalikan bisa berasal dari a atau dari b, dan Rust tidak bisa tahu yang mana hanya dengan membaca fungsinya. Akibatnya, Rust tidak bisa menjamin referensi yang kamu terima masih valid saat kamu memakainya.

Kamu memperbaikinya dengan memberi nama sebuah lifetime (menurut konvensi, huruf kecil seperti 'a) dan memberi tahu Rust bahwa lifetime nilai kembalian terikat pada kedua parameter:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("halo");
let s2 = String::from("hai");
let result = longest(&s1, &s2);
println!("Terpanjang: {result}");
}

Output:

Terpanjang: halo
▶ Coba sendiri

Bacalah 'a sebagai sebuah label, bukan nilai: ia tidak menentukan durasi yang sebenarnya. Ia hanya memberi tahu Rust bahwa “semua referensi ini harus tetap valid setidaknya sepanjang bagian kode yang sama.” Compiler lalu memeriksa setiap tempat fungsi itu dipanggil untuk memastikan hal itu memang benar.

Tanpa anotasi ini, compiler benar-benar tidak bisa tahu apakah referensi yang dikembalikan longest mungkin hidup lebih lama daripada salah satu string asalnya. Anotasi lifetime-lah yang membuatnya bisa membuktikan, saat kompilasi dan tanpa biaya runtime sama sekali, bahwa referensi yang kamu terima selalu aman dipakai.

Tempat berikutnya kamu akan bertemu lifetime adalah di struct. Sejauh ini, setiap field struct memiliki datanya sendiri, misalnya sebuah String. Field juga bisa berupa referensi, tapi kalau begitu struct-nya hanya valid selama data yang ditunjuknya masih ada. Rust memintamu menuliskan hal itu dengan sebuah lifetime:

struct Highlight<'a> {
text: &'a str, // meminjam dari String yang hidup di tempat lain
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Kutipan: {}", first.text);
println!("Seluruh buku panjangnya {} byte", book.len());
}

Output:

Kutipan: Call me Ishmael
Seluruh buku panjangnya 34 byte
▶ Coba sendiri

Bacalah Highlight<'a> sebagai “sebuah Highlight yang meminjam sesuatu selama lifetime 'a”. Bagian &book[..15] adalah sebuah slice: ia meminjam 15 byte pertama dari book tanpa menyalinnya. Karena first hanya meminjam, book masih bisa dipakai sesudahnya. (Teks bukunya sengaja dibiarkan dalam bahasa Inggris, karena hasil slice dan jumlah byte-nya bergantung pada teks itu.)

Lifetime inilah yang membuat Rust bisa menangkap kesalahan berikut, di mana String-nya dibuang selagi struct masih menunjuk ke dalamnya:

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // book dibuang di sini...
println!("{}", highlight.text); // ...tapi highlight masih menunjuk ke dalamnya
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // book dibuang di sini...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...tapi highlight masih menunjuk ke dalamnya
| -------------- borrow later used here

Compiler bilang book tidak hidup cukup lama: ia dibuang di baris 10 selagi masih dipinjam, padahal pinjamannya masih dipakai di baris 12.

Tips praktis: kalau lifetime di struct mulai merepotkan, simpan String yang dimiliki sendiri alih-alih &str. Teksnya memang jadi disalin, tapi struct-nya tidak lagi bergantung pada apa pun, dan lifetime-nya hilang. Banyak programmer Rust berpengalaman memulai dengan cara itu dan baru beralih ke referensi kalau ada alasannya.

Kamu akan jauh lebih jarang menulis 'a daripada yang mungkin terkesan dari halaman ini. Banyak kode Rust yang tidak pernah butuh lifetime eksplisit, karena compiler bisa menyimpulkannya di hampir semua situasi sehari-hari. Kamu terutama akan bertemu lifetime saat sebuah fungsi mengembalikan referensi dan compiler tidak bisa menebak sendiri dari mana asalnya. Saat itu terjadi, ingat saja: itu bukan bug, itu compiler yang memintamu memastikan sesuatu yang tidak bisa ia buktikan sendiri.

Tulis fungsi shortest yang menerima dua string slice dan mengembalikan yang lebih pendek. Kalau panjangnya sama, kembalikan yang pertama. Panggil dengan "kupu-kupu" dan "semut".

Petunjuk: bentuknya sama dengan longest di atas. Kamu butuh <'a> setelah nama fungsinya, dan 'a di kedua parameter serta di tipe kembaliannya.

Lihat solusi
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("kupu-kupu");
let small = String::from("semut");
println!("Terpendek: {}", shortest(&big, &small));
println!("Kalau seri, yang pertama menang: {}", shortest("sapi", "kuda"));
}

Output:

Terpendek: semut
Kalau seri, yang pertama menang: sapi
▶ Coba sendiri

Slice yang dikembalikan bisa berasal dari a atau dari b, jadi Rust butuh 'a untuk tahu bahwa hasilnya hanya valid selama kedua input masih valid. Memakai <= alih-alih < membuat a yang dipilih kalau panjangnya seri.

Buat struct Quote dengan dua field &str, yaitu text dan author. Buat satu instance di mana text meminjam 20 byte pertama dari String "Fearless concurrency is a goal of Rust", dan author adalah literal "The Rust Book". Cetak sebagai Fearless concurrency (The Rust Book).

Petunjuk: seperti Highlight di atas, struct ini butuh lifetime: struct Quote<'a>, dengan &'a str untuk kedua field-nya. &speech[..20] adalah slice dari 20 byte pertama.

Lihat solusi
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

Output:

Fearless concurrency (The Rust Book)
▶ Coba sendiri

quote meminjam dari speech, jadi ia tidak boleh hidup lebih lama darinya. Literal "The Rust Book" tertanam di dalam program dan hidup selama program berjalan, jadi ia jelas bertahan cukup lama untuk 'a. Coba buat speech di dalam blok { } dan cetak kutipannya setelah blok itu berakhir, seperti contoh error di atas, lalu lihat bagaimana compiler menghentikanmu.

  • Lifetime memberi tahu compiler bagaimana referensi-referensi saling berhubungan, supaya ia bisa mencegah dangling reference.
  • Kamu hanya perlu menuliskannya kalau compiler tidak bisa menyimpulkannya sendiri, paling sering pada fungsi yang mengembalikan referensi.
  • 'a tidak menentukan durasi; ia menghubungkan lifetime beberapa referensi supaya compiler bisa memeriksanya.
  • Struct yang menyimpan referensi juga butuh lifetime (struct Highlight<'a>), dan tidak boleh hidup lebih lama dari data yang dipinjamnya. Menyimpan String yang dimiliki sendiri menghindari hal itu.
  • Semua ini murni pemeriksaan saat kompilasi; tidak ada biaya tambahan saat runtime.

Kuis singkat

1. Masalah apa yang diselesaikan oleh lifetime?

2. Kenapa fn longest(a: &str, b: &str) -> &str gagal dikompilasi tanpa anotasi lifetime?

3. Apakah menambahkan anotasi lifetime menambah biaya saat runtime?

4. Menurut halaman ini, kapan kamu paling sering perlu menulis lifetime secara eksplisit?

Skor: 0 / 4