Setiap reference yang kita buat memiliki umur: rentang program selama reference tersebut dijamin masih menunjuk ke data yang hidup. Rust menyebut rentang ini dengan istilah lifetime, dan sepanjang section ini istilah tersebut akan menjadi kosakata utama kita. Sebenarnya kita sudah berhadapan dengan lifetime sejak section Ownership dan Memory — setiap penolakan E0597 bertuliskan borrowed value does not live long enough adalah hasil pembacaan lifetime oleh borrow checker. Sekarang kita membalik arah pandangnya: alih-alih menerima penolakan apa adanya, kita akan membedah cara borrow checker menghitung umur sebuah pinjaman, khususnya pinjaman yang menyeberangi batas fungsi. Pemahaman mekanisme ini menjadi pintu masuk menuju anotasi lifetime yang akan kita tulis pada Chapter-Chapter berikutnya.
Dua Garis Waktu yang Harus Sejajar #
Setiap peminjaman melibatkan dua garis waktu. Garis pertama adalah umur data: sejak pernyataan let menciptakan pemilik sampai pemilik keluar scope dan men-drop nilainya. Garis kedua adalah umur pinjaman: sejak reference dibuat sampai pemakaian terakhirnya. Satu prinsip menjaga keduanya: pinjaman harus berada sepenuhnya di dalam umur data yang dipinjamnya. Borrow checker bekerja dengan membandingkan kedua garis waktu tersebut untuk setiap reference di program kita.
Satu detail penting sebelum lanjut: umur pinjaman berakhir pada pemakaian terakhir, bukan pada akhir scope variabelnya. Perilaku ini resminya disebut non-lexical lifetimes dan berlaku sejak Rust 2018. Akibatnya, sebuah reference yang selesai dipakai di baris kelima dianggap mati di baris kelima, meskipun variabel penampungnya secara teknis masih terlihat sampai akhir blok. Mari kita lihat peminjaman yang tertib dalam praktik:
fn main() {
let novel = String::from("Belajar Rust itu menyenangkan");
let summary = &novel[..7];
println!("Ringkasan: {}", summary);
println!("Novel utuh: {}", novel);
}Output yang Dihasilkan:
Ringkasan: Belajar
Novel utuh: Belajar Rust itu menyenangkanMari kita bedah kode di atas:
- Baris 2 (
let novel = String::from("Belajar Rust itu menyenangkan");):novelmenjadi pemilik tunggal teks di heap. Umur data dimulai di baris ini dan akan berakhir saatmainselesai. - Baris 3 (
let summary = &novel[..7];): ekspresi&novel[..7]membuat string slice yang meminjam tujuh byte pertama miliknovel. Pinjaman dimulai di sini; kepemilikan tetap berada di tangannovel. - Baris 5 (
println!("Ringkasan: {}", summary);): pemakaian pertama sekaligus terakhir atassummary. Pinjaman dianggap berakhir pada baris ini, bukan pada baris penutupmain. - Baris 6 (
println!("Novel utuh: {}", novel);):novelmasih hidup dan utuh — bukti bahwa pemilik tidak terganggu oleh peminjaman. Setelahmainberakhir,noveldi-drop bersama datanya.
Urutan kejadian pada program di atas dapat digambarkan sebagai berikut:
Diagram di atas memperlihatkan inti pemeriksaan borrow checker: pinjaman (tahap B sampai C) berada sepenuhnya di dalam umur data (tahap A sampai D). Selama urutan seperti itu terjaga, peminjaman boleh terjadi sebanyak apa pun. Masalah muncul begitu urutan dibalik — data mati lebih dulu sementara pinjamannya masih dipakai — dan itulah kasus yang akan kita lihat berikutnya.
Ketika Pinjaman Menyeberangi Kematian Pemilik #
Fungsi yang mengembalikan pinjaman dari parameternya adalah pola yang sudah akrab bagi kita — first_word di section Slice dan Referensi bekerja dengan cara itu. Sekarang kita ambil fungsi serupa bernama make_summary yang mengembalikan potongan tujuh byte pertama dari teks yang dipinjamkan kepadanya. Definisi fungsinya sah, tetapi cara kita memanggilnya berikut ini tidak — kode ini tidak dapat dikompilasi:
fn make_summary(text: &str) -> &str {
&text[..7]
}
fn main() {
let summary;
{
let novel = String::from("Belajar Rust itu menyenangkan");
summary = make_summary(&novel);
}
println!("Ringkasan: {}", summary);
}Pesan error kompilator:
error[E0597]: `novel` does not live long enough
--> main.rs:10:32
|
9 | let novel = String::from("Belajar Rust itu menyenangkan");
| ----- binding `novel` declared here
10 | summary = make_summary(&novel);
| ^^^^^^ borrowed value does not live long enough
11 | }
| - `novel` dropped here while still borrowed
12 |
13 | println!("Ringkasan: {}", summary);
| ------- borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0597`.Pesan E0597 kembali menceritakan kronologi pelanggaran, dan kali ini pelanggarannya terjadi lewat jalur nilai kembalian fungsi:
- Baris 9–10 pada pesan (
binding 'novel' declared heredanborrowed value does not live long enough):novelhidup di dalam scope blok, lalu dipinjam oleh pemanggilanmake_summary(&novel). Reference yang dikembalikan fungsi disimpan kesummarydi scope luar. - Baris 11 pada pesan (
`- `novel` dropped here while still borrowed`): blok dalam ditutup, pemiliknovelkeluar scope, dan data heap miliknoveldi-drop — padahal pinjaman melaluisummarymasih beredar. - Baris 13 pada pesan (
borrow later used here):println!memakaisummarysetelah kematian pemiliknya. Inilah pemakaian yang menjadikan pinjaman dianggap masih hidup melampaui umur datanya.
Kontrak pada Tanda Tangan Fungsi #
Pertanyaan menariknya: dari mana borrow checker tahu bahwa summary meminjam novel, padahal peminjaman terjadi di dalam tubuh make_summary yang sudah selesai dieksekusi? Jawabannya ada pada tanda tangan fn make_summary(text: &str) -> &str. Tanda tangan tersebut berfungsi sebagai kontrak: fungsi menerima satu parameter reference dan mengembalikan satu reference, sehingga satu-satunya kandidat sumber pinjaman bagi nilai kembalian adalah parameter text. Borrow checker mencatat hubungan itu, lalu di titik pemanggilan menghitung: nilai kembalian ikut meminjam novel, pinjaman hidup sampai pemakaian terakhir summary, dan novel mati lebih dulu — pelanggaran.
Pola satu parameter seperti di atas mudah disimpulkan kompilator. Kesulitan muncul ketika sebuah fungsi menerima dua parameter reference dan mengembalikan satu reference: sumber pinjaman yang mana? Tubuh fungsi bisa memilih parameter pertama pada satu cabang logika dan parameter kedua pada cabang lainnya, dan pemanggil berhak tahu hubungan umur tersebut tanpa membaca isi fungsi. Ketika hubungan itu tidak bisa ditebak, kompilator menolak menebak dan meminta kita menuliskannya secara eksplisit — itulah E0106 yang akan kita jumpai di pembuka Chapter berikutnya.
Ringkasan #
Lifetime adalah rentang program selama sebuah reference dijamin valid, dan borrow checker membandingkan dua garis waktu — umur data milik pemilik dan umur pinjaman milik reference — untuk setiap peminjaman. Pinjaman yang selesai dipakai sebelum pemiliknya mati diterima; pinjaman yang dipakai setelah data di-drop ditolak sebagai E0597, termasuk pinjaman yang dibawa keluar lewat nilai kembalian fungsi. Untuk kasus satu parameter reference, kompilator menyimpulkan sendiri hubungan sumber pinjaman dari tanda tangan. Untuk kasus yang tidak bisa ditebak, kita perlu menuliskan hubungan tersebut secara eksplisit — bahasa yang akan kita pelajari di Chapter Anotasi Lifetime di Fungsi.