Chapter sebelumnya berakhir pada satu pertanyaan yang belum terjawab: fungsi dengan dua parameter reference dan satu nilai kembalian reference — kompilator tidak bisa menebak nilai kembalian meminjam dari parameter yang mana. Kali ini kita akan berhadapan langsung dengan pertanyaan tersebut lewat fungsi pencari teks terpanjang, membaca penolakan E0106 yang menolak menebak, lalu menyelesaikannya dengan lifetime annotation berbentuk <'a>. Di sepanjang jalan kita akan membedah apa yang sebenarnya dijanjikan anotasi tersebut di titik pemanggilan — dan membuktikan bahwa anotasi tidak pernah mengubah umur referensi.

Fungsi yang Umurnya Tidak Bisa Ditebak #

Fungsi first_longest menerima dua teks dan mengembalikan teks yang lebih panjang di antara keduanya; jika sama panjang, teks pertama yang menang — nama first_longest kita pilih untuk menegaskan aturan seri tersebut. Nilai kembalian jelas berupa pinjaman: tidak ada alokasi baru, fungsi hanya menyerahkan potongan data yang sudah ada milik pemanggil. Versi tanpa anotasi berikut ini tidak dapat dikompilasi:

rust
fn first_longest(x: &str, y: &str) -> &str {
    if x.len() >= y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let text1 = String::from("Rust");
    let text2 = String::from("Belajar Rust itu menyenangkan");

    let result = first_longest(&text1, &text2);
    println!("Teks terpanjang: {}", result);
}

Pesan error kompilator:

text
error[E0106]: missing lifetime specifier
 --> main.rs:1:39
  |
1 | fn first_longest(x: &str, y: &str) -> &str {
  |                     ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
1 | fn first_longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |                 ++++     ++          ++          ++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0106`.

Baris bantuan pada pesan tersebut menyebut inti masalahnya dengan gamblang: tipe kembalian memuat nilai pinjaman, tetapi tanda tangan tidak menyatakan pinjaman itu berasal dari x atau dari y. Borrow checker memeriksa pemanggilan tanpa membaca tubuh fungsi, dan tubuh fungsi memang tidak bisa dijadikan pegangan — cabang if pada suatu versi bisa mengembalikan x, lalu versi berikutnya mengembalikan y; hubungan umur harus tetap terbaca dari tanda tangan agar semua pemanggil dapat dipercaya tanpa ikut membaca isi. Kompilator pun menolak menebak dan menyodorkan bentuk perbaikan lengkap dengan posisi tanda tambahnya.

Perbaikan yang disodorkan pesan error memperkenalkan generic lifetime parameter <'a>. Bentuknya sengaja dibuat paralel dengan generic type parameter yang sudah kita kenal: jika <T> mendeklarasikan tipe yang diisi belakangan, <'a> mendeklarasikan umur yang diisi belakangan. Tanda petik satu membedakan lifetime parameter dari type parameter, dan nama setelahnya bebas asal dimulai apostrof — <'a> hanyalah kebiasaan yang sama umumnya dengan T untuk tipe. Berikut versi yang sah:

rust
fn first_longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let text1 = String::from("Rust");
    let text2 = String::from("Belajar Rust itu menyenangkan");

    let result = first_longest(text1.as_str(), text2.as_str());
    println!("Teks terpanjang: {}", result);
}

Output yang Dihasilkan:

text
Teks terpanjang: Belajar Rust itu menyenangkan

Mari kita bedah kode di atas:

  1. Baris 1 (fn first_longest<'a>(x: &'a str, y: &'a str) -> &'a str): deklarasi <'a> memperkenalkan satu lifetime parameter. Ketiga reference pada tanda tangan — x, y, dan nilai kembalian — kini bertipe reference yang umurnya terikat pada a yang sama.
  2. Baris 2–6 (tubuh percabangan if): kedua cabang mengembalikan x atau y, keduanya bertipe &'a str, sehingga tubuh fungsi konsisten dengan janji tanda tangan.
  3. Baris 10–11 (let text1 = ... dan let text2 = ...): main menjadi pemilik kedua String; masing-masing hidup sampai akhir main.
  4. Baris 13 (let result = first_longest(text1.as_str(), text2.as_str());): di titik pemanggilan, kompilator mengisi a dengan umur konkret. Karena x dan y berbagi satu a, umur yang terpilih adalah irisan umur kedua pinjaman — umur bersama terpendek. Dengan demikian result dijamin valid selama kedua teks sumbernya masih valid.
  5. Baris 14 (println!("Teks terpanjang: {}", result);): pemakaian result berlangsung ketika text1 dan text2 sama-sama masih hidup — kontrak terpenuhi.

Satu pertanyaan yang wajar muncul di titik ini: mengapa x dan y dipaksa berbagi satu a? Bentuk alternatifnya memang ada — misalnya fn first_longest<'a, 'b>(x: &'a str, y: &'b str) — tetapi bentuk itu justru tidak bisa dipakai untuk fungsi ini. Nilai kembalian harus menyatakan satu umur, sedangkan tubuh fungsi bisa mengembalikan x atau y tergantung cabangnya; jika umur x dan umur y dinyatakan saling lepas, tidak ada satu umur pun yang sah mewakili keduanya sekaligus. Satu-satunya kontrak yang benar untuk first_longest adalah “hasil hidup selama kedua masukan masih hidup”, dan kontrak itulah yang dituliskan &'a str pada ketiga posisi. Jumlah dan bentuk parameter lifetime dengan demikian bukan urusan selera penulisan — bentuknya mengikuti janji yang benar-benar bisa dipenuhi tubuh fungsi. Kelak di Chapter Lifetime Elision kita akan bertemu fungsi print_both yang justru tepat memakai dua umur terpisah karena tidak mengembalikan reference apa pun.

Kontrak yang Ditegakkan di Titik Pemanggilan #

Bukti terbaik bahwa anotasi tidak memperpanjang umur adalah menjegalnya dengan data yang sengaja berumur pendek. Program berikut tetap memakai first_longest versi beranotasi, tetapi text2 kini hidup di dalam blok dalam sementara result dipakai setelah blok ditutup — kode ini juga tidak dapat dikompilasi:

rust
fn first_longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let text1 = String::from("Rust");
    let result;

    {
        let text2 = String::from("Belajar Rust itu menyenangkan");
        result = first_longest(text1.as_str(), text2.as_str());
    }

    println!("Teks terpanjang: {}", result);
}

Pesan error kompilator:

text
error[E0597]: `text2` does not live long enough
  --> main.rs:15:48
   |
14 |         let text2 = String::from("Belajar Rust itu menyenangkan");
   |             ----- binding `text2` declared here
15 |         result = first_longest(text1.as_str(), text2.as_str());
   |                                                ^^^^^ borrowed value does not live long enough
16 |     }
   | - `text2` dropped here while still borrowed
17 |
18 |     println!("Teks terpanjang: {}", result);
   |                                     ------ borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0597`.

Urutan kejadian pada program gagal tersebut dapat digambarkan sebagai berikut:

Diagram di atas memperlihatkan penentuan a sebagai umur bersama terpendek: text1 berumur panjang, text2 berumur pendek, sehingga irisan keduanya hanyalah rentang kehidupan text2. Pinjaman result pun terikat pada rentang tersebut. Pemakaian result di baris 18 terjadi setelah text2 di-drop di baris 16 — pelanggaran yang ditangkap dengan pesan E0597, kini dilengkapi penunjukan variabel mana yang umurnya kurang.

Anotasi justru membuat penegakan menjadi lebih tajam. Karena kontrak “hasil hidup tidak lebih lama dari parameter” tertulis eksplisit, borrow checker dapat menunjuk text2 sebagai pihak yang menggagalkan kontrak. Solusinya bukan menambah anotasi lagi, melainkan memperbaiki urutan umur: pindahkan println! ke dalam blok — kita sudah memverifikasi bahwa versi dengan pemakaian di dalam blok dikompilasi dan berjalan baik. Pinjaman boleh pendek, pemilik harus lebih panjang umurnya; aturan itu tidak pernah berubah.

Ringkasan #

Generic lifetime parameter <'a> memberi tanda tangan fungsi bahasa untuk menyatakan hubungan umur: fn first_longest<'a>(x: &'a str, y: &'a str) -> &'a str menjanjikan bahwa nilai kembalian hidup tidak lebih lama dari umur bersama kedua parameter. Tanpa anotasi, tanda tangan dua parameter seperti itu ditolak sebagai E0106 karena kompilator menolak menebak sumber pinjaman. Dengan anotasi, kontrak justru ditegakkan lebih presisi — pemanggilan dengan data berumur pendek tertangkap sebagai E0597 lengkap dengan nama variabel pelanggar. Anotasi sendiri tidak mengubah umur referensi sedikit pun; anotasi hanya mendeskripsikan hubungan agar bisa divalidasi. Berikutnya kita membawa bahasa ini ke tempat yang menyimpan data lebih lama: field struct.