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

Memvalidasi Referensi dengan Lifetimes

Lifetimes (waktu hidup) adalah jenis generik lain yang sebenarnya sudah kita pakai. Bukannya memastikan kalau sebuah tipe punya perilaku yang kita mau, lifetimes memastikan kalau referensi bakal tetap valid selama kita masih butuh.

Satu detail yang tidak kita bahas di bagian “Referensi dan Borrowing” di Bab 4 adalah setiap referensi di Rust punya sebuah lifetime, yaitu scope di mana referensi itu valid. Sebagian besar waktu, lifetimes itu bersifat implisit dan ditebak (inferred), sama halnya kayak sebagian besar waktu tipe juga ditebak. Kita baru diwajibkan buat menganotasi tipe kalau ada beberapa kemungkinan tipe yang bisa dipakai. Mirip dengan itu, kita harus menganotasi lifetimes kalau lifetimes dari referensi-referensi yang ada bisa berhubungan dengan beberapa cara yang berbeda. Rust mewajibkan kita menganotasi hubungan ini memakai parameter lifetime generik untuk memastikan referensi sebenarnya yang dipakai pas runtime bakal pasti valid.

Menganotasi lifetimes bahkan bukan konsep yang dimiliki kebanyakan bahasa pemrograman lain, jadi ini mungkin bakal terasa asing. Meskipun kita tidak bakal membahas lifetimes secara menyeluruh di bab ini, kita bakal membahas cara-cara umum yang mungkin bakal kita temui terkait sintaks lifetime supaya kita bisa nyaman dengan konsepnya.

Mencegah Dangling References dengan Lifetimes

Tujuan utama dari lifetimes adalah buat mencegah dangling references (referensi menggantung), yang bikin program merujuk ke data yang salah alih- alih data yang sebenarnya dituju. Coba perhatikan program di Listing 10-16, yang punya scope luar dan scope dalam.

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
Listing 10-16: Usaha untuk memakai referensi yang nilainya sudah keluar dari scope

Catatan: Contoh di Listing 10-16, 10-17, dan 10-23 mendeklarasikan variabel tanpa memberi mereka nilai awal, jadi nama variabelnya eksis di scope luar. Sekilas, ini mungkin kelihatannya bertentangan sama aturan Rust yang tidak mengizinkan nilai null. Tapi, kalau kita mencoba memakai sebuah variabel sebelum memberinya nilai, kita bakal dapat error compile-time, yang menunjukkan kalau Rust memang tidak mengizinkan nilai null.

Scope luar mendeklarasikan variabel bernama r tanpa nilai awal, dan scope dalam mendeklarasikan variabel bernama x dengan nilai awal 5. Di dalam scope dalam, kita mencoba nge-set nilai r jadi referensi ke x. Kemudian scope dalamnya berakhir, dan kita mencoba mencetak nilai di r. Kode ini tidak bakal bisa di-compile karena nilai yang dirujuk sama r sudah keluar dari scope sebelum kita mencoba memakainya. Ini pesan error-nya:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

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

Pesan error-nya bilang kalau variabel x “tidak hidup cukup lama” (does not live long enough). Alasannya adalah x bakal keluar dari scope saat scope dalam berakhir di baris 7. Tapi r masih valid untuk scope luar; karena scope-nya lebih besar, kita bilang kalau dia “hidup lebih lama.” Kalau Rust membiarkan kode ini jalan, r bakal merujuk ke memori yang sudah di-dealokasi saat x keluar dari scope, dan apa pun yang kita coba lakukan dengan r tidak bakal jalan dengan benar. Terus gimana caranya Rust bisa nentuin kalau kode ini tidak valid? Rust memakai sebuah borrow checker.

Borrow Checker

Compiler Rust punya sebuah borrow checker yang membandingkan scopes buat menentukan apakah semua referensi yang dipinjam (borrows) itu valid. Listing 10-17 menunjukkan kode yang sama seperti Listing 10-16 tapi dengan anotasi yang menunjukkan lifetimes dari variabel-variabelnya.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Listing 10-17: Anotasi lifetimes dari r dan x, dinamakan masing-masing 'a dan 'b

Di sini, kita sudah menganotasi lifetime dari r dengan 'a dan lifetime dari x dengan 'b. Seperti yang bisa dilihat, blok 'b di dalam itu jauh lebih kecil daripada blok lifetime 'a di luar. Pada saat compile time, Rust membandingkan ukuran dari dua lifetimes ini dan melihat kalau r punya lifetime 'a tapi dia merujuk ke memori dengan lifetime 'b. Programnya ditolak karena 'b lebih pendek dari 'a: subjek yang dirujuk tidak hidup selama referensinya.

Listing 10-18 memperbaiki kodenya biar dia tidak punya dangling reference dan bisa di-compile tanpa error sama sekali.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+
Listing 10-18: Referensi yang valid karena datanya punya lifetime yang lebih panjang dari referensinya

Di sini, x punya lifetime 'b, yang mana di kasus ini lebih besar dari 'a. Ini berarti r bisa merujuk ke x karena Rust tahu kalau referensi di r bakal selalu valid selama x masih valid.

Sekarang setelah kita tahu di mana lifetimes dari referensi berada dan gimana Rust menganalisis lifetimes buat memastikan referensi bakal selalu valid, mari kita eksplor lifetimes generik buat parameter dan nilai kembalian di dalam konteks fungsi.

Generic Lifetimes di Fungsi

Kita bakal nulis fungsi yang mengembalikan string slice yang lebih panjang di antara dua string slice. Fungsi ini bakal menerima dua string slice dan mengembalikan satu string slice. Setelah kita mengimplementasikan fungsi longest, kode di Listing 10-19 seharusnya mencetak The longest string is abcd.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}
Listing 10-19: Fungsi main yang memanggil fungsi longest buat mencari mana yang lebih panjang dari dua string slices

Perhatikan bahwa kita mau fungsi ini menerima string slices, yang merupakan referensi, bukannya strings, karena kita tidak mau fungsi longest mengambil ownership dari parameter-parameternya. Coba cek lagi bagian “String Slices sebagai Parameter” di Bab 4 buat pembahasan lebih lanjut soal kenapa parameter yang kita pakai di Listing 10-19 adalah yang memang kita perlukan.

Kalau kita mencoba mengimplementasikan fungsi longest seperti yang ditunjukkan di Listing 10-20, kode ini tidak bakal bisa di-compile.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-20: Implementasi fungsi longest yang mengembalikan yang lebih panjang dari dua string slices tapi belum bisa di-compile

Alih-alih jalan, kita dapat error berikut yang membahas soal lifetimes:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn 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
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

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

Teks bantuannya ngasih tau kalau tipe kembaliannya butuh parameter lifetime generik di situ karena Rust tidak bisa menebak apakah referensi yang bakal dikembalikan itu merujuk ke x atau ke y. Nyatanya, kita juga tidak tahu, karena blok if di dalam body fungsi ini mengembalikan referensi ke x dan blok else mengembalikan referensi ke y!

Saat mendefinisikan fungsi ini, kita tidak tahu nilai konkret apa yang bakal dimasukkan ke fungsi ini, jadi kita tidak tahu apakah blok if atau blok else yang bakal dijalankan. Kita juga tidak tahu lifetimes konkret dari referensi yang bakal dimasukkan, jadi kita tidak bisa melihat scopes seperti yang kita lakukan di Listing 10-17 dan 10-18 untuk menentukan apakah referensi yang kita kembalikan itu bakal selalu valid. Borrow checker juga tidak bisa menentukannya, karena dia tidak tahu gimana lifetimes dari x dan y berhubungan sama lifetime dari nilai kembaliannya. Buat membenarkan error ini, kita bakal menambahkan parameter lifetime generik yang mendefinisikan hubungan antara referensi-referensi tersebut agar borrow checker bisa melakukan analisisnya.

Sintaks Anotasi Lifetime

Anotasi lifetime tidak mengubah seberapa lama suatu referensi hidup. Mereka justru menggambarkan hubungan dari lifetimes antara banyak referensi satu sama lain tanpa memengaruhi lifetimes itu sendiri. Sama seperti fungsi yang bisa menerima tipe apa pun saat signature-nya menentukan parameter tipe generik, fungsi juga bisa menerima referensi dengan lifetime apa pun dengan menentukan parameter lifetime generik.

Anotasi lifetime punya sintaks yang agak tidak biasa: nama parameter lifetime harus dimulai dengan apostrof (tanda kutip tunggal, ') dan biasanya semuanya huruf kecil dan sangat pendek, sama seperti tipe generik. Kebanyakan orang memakai nama 'a untuk anotasi lifetime yang pertama. Kita menaruh anotasi parameter lifetime setelah tanda & dari sebuah referensi, dengan memakai spasi untuk memisahkan anotasinya dari tipe referensinya.

Ini beberapa contohnya: sebuah referensi ke i32 tanpa parameter lifetime, sebuah referensi ke i32 yang punya parameter lifetime bernama 'a, dan sebuah referensi mutable ke i32 yang juga punya lifetime 'a.

&i32        // sebuah referensi
&'a i32     // sebuah referensi dengan _lifetime_ eksplisit
&'a mut i32 // sebuah referensi _mutable_ dengan _lifetime_ eksplisit

Satu anotasi lifetime yang berdiri sendiri tidak punya banyak arti karena anotasi itu ditujukan untuk memberi tahu Rust gimana parameter lifetime generik dari berbagai referensi saling berhubungan. Mari kita teliti gimana anotasi lifetime berhubungan satu sama lain di dalam konteks fungsi longest.

Anotasi Lifetime di Signature Fungsi

Buat memakai anotasi lifetime di signature fungsi, kita harus mendeklarasikan parameter lifetime generik di dalam kurung sudut di antara nama fungsi dan daftar parameter, sama persis seperti yang kita lakukan sama parameter tipe generik.

Kita mau signature ini mengekspresikan batasan ini: referensi yang dikembalikan bakal valid setidaknya selama kedua parameter itu juga valid. Ini adalah hubungan antara lifetimes dari parameter dan nilai kembaliannya. Kita bakal menamakan lifetime itu 'a lalu menambahkannya ke setiap referensi, seperti yang ditunjukkan di Listing 10-21.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-21: Definisi fungsi longest yang menentukan kalau semua referensi di signature tersebut harus punya lifetime 'a yang sama

Kode ini seharusnya bisa di-compile dan menghasilkan hasil yang kita inginkan saat kita memakainya bersama fungsi main di Listing 10-19.

Signature fungsinya sekarang memberi tahu Rust bahwa untuk suatu lifetime 'a, fungsi ini menerima dua parameter, di mana keduanya adalah string slices yang hidup setidaknya sepanjang lifetime 'a. Signature fungsinya juga memberi tahu Rust bahwa string slice yang dikembalikan dari fungsi itu bakal hidup setidaknya sepanjang lifetime 'a. Di praktiknya, ini berarti lifetime dari referensi yang dikembalikan oleh fungsi longest itu sama dengan lifetime yang paling kecil dari antara nilai-nilai yang dirujuk oleh argumen-argumen fungsi tersebut. Hubungan-hubungan inilah yang kita mau Rust pakai saat menganalisis kode ini.

Ingat, saat kita menentukan parameter lifetime di signature fungsi ini, kita tidak sedang mengubah lifetimes dari nilai apa pun yang masuk atau keluar. Tapi, kita sedang menentukan kalau borrow checker harus menolak nilai apa pun yang tidak mematuhi batasan-batasan ini. Perhatikan bahwa fungsi longest tidak perlu tahu persis berapa lama x dan y bakal hidup, dia cuma perlu tahu kalau ada suatu scope yang bisa disubstitusi untuk 'a yang bakal memenuhi signature ini.

Saat menganotasi lifetimes di fungsi, anotasinya ditaruh di signature fungsi, bukan di body fungsi. Anotasi lifetime menjadi bagian dari kontrak fungsi itu, mirip dengan tipe-tipe di signature-nya. Memiliki signature fungsi yang mengandung kontrak lifetime berarti analisis yang dilakukan compiler Rust bisa jadi lebih sederhana. Kalau ada masalah dengan cara sebuah fungsi dianotasi atau cara dia dipanggil, pesan error compiler bisa menunjuk ke bagian kode kita serta batasannya dengan lebih tepat. Kalau sebaliknya compiler Rust menebak-nebak lebih banyak soal apa yang kita maksud terkait hubungan antar lifetimes, compiler mungkin cuma bakal bisa nunjukin pemakaian kode kita yang berjarak beberapa langkah dari sumber masalah aslinya.

Saat kita memasukkan referensi konkret ke longest, lifetime konkret yang disubstitusikan untuk 'a adalah bagian dari scope x yang tumpang tindih (overlap) dengan scope y. Dengan kata lain, lifetime generik 'a bakal mendapatkan lifetime konkret yang setara dengan yang lebih kecil di antara lifetimes x dan y. Karena kita sudah menganotasi referensi yang dikembalikan dengan parameter lifetime 'a yang sama, referensi kembalian itu juga bakal valid sepanjang yang lebih kecil di antara lifetimes x dan y.

Mari kita lihat gimana anotasi lifetime membatasi fungsi longest dengan memasukkan referensi yang punya lifetimes konkret yang berbeda. Listing 10-22 adalah contoh yang simpel.

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-22: Memakai fungsi longest dengan referensi ke nilai String yang punya lifetimes konkret yang berbeda

Di contoh ini, string1 itu valid sampai akhir dari scope luar, string2 itu valid sampai akhir scope dalam, dan result merujuk ke sesuatu yang valid sampai akhir scope dalam. Jalankan kode ini dan kita bakal lihat kalau borrow checker menyetujuinya; kodenya bakal di-compile dan mencetak The longest string is long string is long.

Berikutnya, mari kita coba contoh yang menunjukkan kalau lifetime dari referensi di result harus merupakan lifetime yang lebih kecil dari dua argumennya. Kita bakal memindahkan deklarasi variabel result ke luar scope dalam tapi membiarkan proses assignment nilai ke variabel result di dalam scope bareng string2. Lalu kita bakal pindahin println! yang memakai result ke luar scope dalam, setelah scope dalam tersebut berakhir. Kode di Listing 10-23 tidak bakal bisa di-compile.

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-23: Mencoba memakai result setelah string2 keluar dari scope

Pas kita nyoba compile kode ini, kita bakal dapet error ini:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

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

Error ini menunjukkan kalau biar result valid buat statement println!, string2 harusnya valid sampai akhir dari scope luar. Rust tahu ini karena kita sudah menganotasi lifetimes dari parameter fungsi dan nilai kembalian dengan memakai parameter lifetime 'a yang sama.

Sebagai manusia, kita bisa melihat kode ini dan langsung tahu kalau string1 lebih panjang dari string2, dan karenanya, result bakal berisi referensi ke string1. Karena string1 belum keluar dari scope, referensi ke string1 seharusnya masih valid buat statement println!. Tapi, compiler tidak bisa melihat kalau referensinya valid di kasus ini. Kita sudah memberi tahu Rust kalau lifetime referensi yang dikembalikan oleh fungsi longest itu sama dengan yang lebih kecil di antara lifetimes dari referensi-referensi yang dimasukkan. Maka dari itu, borrow checker menolak kode di Listing 10-23 karena kemungkinannya punya referensi yang tidak valid.

Coba desain eksperimen lain yang memvariasikan nilai dan lifetimes dari referensi yang di-pass ke fungsi longest serta gimana referensi kembaliannya dipakai. Bikin hipotesis tentang apakah eksperimen kita bakal lolos borrow checker sebelum men-compile; lalu cek apakah kita benar!

Berpikir dalam Konteks Lifetimes

Gimana cara kita menentukan parameter lifetime itu bergantung pada apa yang sedang dilakukan sama fungsi kita. Misalnya, kalau kita mengubah implementasi fungsi longest biar selalu mengembalikan parameter pertama bukannya string slice yang paling panjang, kita tidak perlu menentukan lifetime pada parameter y. Kode berikut ini bakal bisa di-compile:

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Kita sudah menentukan parameter lifetime 'a buat parameter x dan tipe kembaliannya, tapi tidak buat parameter y, karena lifetime y tidak punya hubungan apa pun dengan lifetime x atau nilai kembaliannya.

Saat mengembalikan sebuah referensi dari sebuah fungsi, parameter lifetime buat tipe kembaliannya harus cocok dengan parameter lifetime buat salah satu dari parameternya. Kalau referensi yang dikembalikan tidak merujuk ke salah satu parameter, maka referensi itu pasti merujuk ke suatu nilai yang dibuat di dalam fungsi itu sendiri. Namun, ini bakal jadi dangling reference karena nilai itu bakal keluar dari scope di akhir dari fungsinya. Coba perhatikan usaha implementasi fungsi longest yang tidak bakal bisa di-compile ini:

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Di sini, meskipun kita sudah menentukan parameter lifetime 'a buat tipe kembaliannya, implementasi ini bakal gagal di-compile karena lifetime nilai kembaliannya sama sekali tidak berhubungan dengan lifetime parameter-parameternya. Ini pesan error yang bakal kita dapat:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

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

Masalahnya adalah result keluar dari scope dan dibersihkan di akhir dari fungsi longest. Kita juga mencoba mengembalikan referensi ke result dari fungsinya. Tidak ada cara buat kita menentukan parameter lifetime yang bisa mengubah dangling reference tersebut, dan Rust tidak bakal ngebiarin kita bikin dangling reference. Di kasus ini, perbaikan terbaiknya adalah dengan mengembalikan tipe data yang owned (dimiliki) bukannya sebuah referensi sehingga fungsi yang memanggilnya nanti bertanggung jawab buat membersihkan nilai tersebut.

Pada akhirnya, sintaks lifetime adalah soal menghubungkan lifetimes dari berbagai parameter dan nilai kembalian dari suatu fungsi. Begitu mereka terhubung, Rust punya informasi yang cukup buat mengizinkan operasi yang aman buat memori (memory-safe operations) dan menolak operasi yang bakal membuat dangling pointers atau melanggar keamanan memori.

Anotasi Lifetime di Definisi Struct

Sejauh ini, struct yang kita definisikan semuanya menampung tipe-tipe yang owned. Kita bisa mendefinisikan struct buat menampung referensi, tapi di kasus itu kita perlu menambahkan anotasi lifetime pada setiap referensi di dalam definisi struct tersebut. Listing 10-24 punya struct bernama ImportantExcerpt yang menampung sebuah string slice.

Filename: src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Listing 10-24: Sebuah struct yang menampung referensi, yang mana butuh anotasi lifetime

Struct ini punya field tunggal part yang menampung string slice, yang merupakan sebuah referensi. Sama kayak tipe data generik, kita mendeklarasikan nama parameter lifetime generik di dalam kurung sudut setelah nama struct sehingga kita bisa memakai parameter lifetime itu di dalam definisi struct-nya. Anotasi ini berarti sebuah instance dari ImportantExcerpt tidak bisa hidup lebih lama dari referensi yang ditampungnya di field part.

Fungsi main di sini bikin instance dari struct ImportantExcerpt yang menampung referensi ke kalimat pertama dari String yang dimiliki sama variabel novel. Data di novel sudah ada sebelum instance ImportantExcerpt itu dibikin. Selain itu, novel belum keluar dari scope sampai setelah ImportantExcerpt keluar dari scope, jadi referensi di dalam instance ImportantExcerpt itu dipastikan valid.

Lifetime Elision (Penghilangan Lifetime)

Kita udah belajar kalau setiap referensi punya lifetime dan kita harus menentukan parameter lifetime untuk fungsi atau struct yang memakai referensi. Namun, kita tadi punya fungsi di Listing 4-9, yang ditampilkan lagi di Listing 10-25, yang berhasil di-compile tanpa anotasi lifetime.

Filename: src/lib.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

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

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 10-25: Sebuah fungsi yang kita definisikan di Listing 4-9 yang berhasil di-compile tanpa anotasi lifetime, biarpun parameter dan tipe kembaliannya berupa referensi

Alasan kenapa fungsi ini bisa di-compile tanpa anotasi lifetime murni karena sejarah: di versi awal (sebelum 1.0) dari Rust, kode ini tidak bakal bisa di-compile karena setiap referensi butuh lifetime yang eksplisit. Waktu itu, signature fungsi ini bakal ditulis kayak gini:

fn first_word<'a>(s: &'a str) -> &'a str {

Setelah menulis banyak kode Rust, tim Rust menemukan kalau programmer Rust memasukkan anotasi lifetime yang sama berulang kali di situasi-situasi tertentu. Situasi-situasi ini bisa diprediksi dan mengikuti beberapa pola yang deterministik. Para pengembang memprogram pola-pola ini ke dalam kode compiler supaya borrow checker bisa menebak (infer) lifetimes di situasi-situasi ini dan tidak membutuhkan anotasi yang eksplisit lagi.

Sejarah Rust ini cukup relevan karena mungkin saja ke depannya bakal ada pola deterministik lain yang muncul dan ditambahkan ke dalam compiler. Di masa depan, mungkin bakal lebih sedikit lagi anotasi lifetime yang diwajibkan.

Pola-pola yang diprogram ke dalam analisis referensi Rust disebut lifetime elision rules (aturan penghilangan lifetime). Ini bukan aturan buat dipatuhi sama programmer; mereka ini adalah serangkaian kasus tertentu yang bakal dipertimbangkan oleh compiler, dan kalau kode kita masuk ke kasus-kasus ini, kita tidak perlu nulis lifetimes-nya secara eksplisit.

Aturan elision ini tidak memberikan tebakan (inference) yang komplit. Kalau masih ada kebingungan atau ketidakpastian (ambiguity) soal apa lifetimes dari referensi tersebut setelah Rust menerapkan aturan-aturannya, compiler tidak bakal menebak-nebak apa seharusnya lifetime untuk referensi yang tersisa. Alih-alih menebak, compiler bakal ngasih kita error yang bisa diselesaikan dengan menambahkan anotasi lifetime secara manual.

Lifetimes pada parameter fungsi atau method disebut input lifetimes, dan lifetimes pada nilai kembalian disebut output lifetimes.

Compiler memakai tiga aturan buat mencari tahu lifetimes dari referensi saat tidak ada anotasi yang eksplisit. Aturan pertama berlaku buat input lifetimes, sedangkan aturan kedua dan ketiga berlaku buat output lifetimes. Kalau compiler sudah sampai ke akhir dari tiga aturan ini dan masih ada referensi yang tidak diketahui lifetimes-nya, compiler bakal berhenti dengan sebuah error. Aturan-aturan ini berlaku buat definisi fn maupun blok impl.

Aturan pertama adalah compiler meng-assign parameter lifetime ke setiap parameter yang berupa referensi. Dengan kata lain, fungsi dengan satu parameter dapat satu parameter lifetime: fn foo<'a>(x: &'a i32); fungsi dengan dua parameter dapat dua parameter lifetime terpisah: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); dan seterusnya.

Aturan kedua adalah, kalau ada tepat satu parameter input lifetime, lifetime itu di-assign ke semua parameter output lifetime: fn foo<'a>(x: &'a i32) -> &'a i32.

Aturan ketiga adalah, kalau ada beberapa parameter input lifetime, tapi salah satunya adalah &self atau &mut self karena ini adalah sebuah method, maka lifetime dari self itu bakal di-assign ke semua parameter output lifetime. Aturan ketiga ini bikin methods jauh lebih enak buat dibaca dan ditulis karena kita butuh lebih sedikit simbol.

Mari pura-pura kita adalah compiler. Kita bakal menerapkan aturan-aturan ini buat mencari tahu lifetimes dari referensi di dalam signature fungsi first_word di Listing 10-25. Signature-nya mulai tanpa ada lifetimes apa pun yang berkaitan dengan referensi-referensinya:

fn first_word(s: &str) -> &str {

Kemudian compiler menerapkan aturan pertama, yang menentukan bahwa tiap parameter dapat lifetime-nya masing-masing. Kita bakal menyebutnya 'a seperti biasa, jadi sekarang signature-nya seperti ini:

fn first_word<'a>(s: &'a str) -> &str {

Aturan kedua bisa diterapkan karena ada tepat satu input lifetime. Aturan kedua menentukan bahwa lifetime dari satu parameter input itu di-assign ke output lifetime, jadi signature-nya sekarang jadi kayak gini:

fn first_word<'a>(s: &'a str) -> &'a str {

Sekarang semua referensi di signature fungsi ini sudah punya lifetimes, dan compiler bisa melanjutkan analisisnya tanpa perlu programmer buat menganotasi lifetimes di signature fungsi ini.

Mari kita lihat contoh lain, kali ini memakai fungsi longest yang tidak punya parameter lifetime pas kita mulai ngerjain di Listing 10-20:

fn longest(x: &str, y: &str) -> &str {

Mari terapkan aturan pertama: tiap parameter dapat lifetime-nya sendiri. Kali ini kita punya dua parameter bukannya satu, jadi kita punya dua lifetimes:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Kita bisa lihat kalau aturan kedua tidak bisa diterapkan karena ada lebih dari satu input lifetime. Aturan ketiga juga tidak bisa diterapkan, karena longest adalah sebuah fungsi bukannya method, jadi tidak ada parameter yang berupa self. Setelah melewati ketiga aturan ini, kita masih belum tahu apa lifetime dari tipe kembaliannya. Inilah alasan kenapa kita dapat error pas nyoba men-compile kode di Listing 10-20: compiler sudah melewati aturan-aturan lifetime elision tapi masih belum bisa mencari tahu semua lifetimes dari referensi yang ada di signature tersebut.

Karena aturan ketiga sebenarnya cuma berlaku buat method signatures, kita bakal membahas lifetimes di konteks tersebut selanjutnya buat melihat kenapa aturan ketiga ini bikin kita tidak perlu menganotasi lifetimes di method signatures terlalu sering.

Anotasi Lifetime di Definisi Method

Saat kita mengimplementasikan methods pada struct yang punya lifetimes, kita memakai sintaks yang sama persis seperti parameter tipe generik, yang ditunjukkan di Listing 10-11. Di mana kita mendeklarasikan dan memakai parameter lifetimes itu bergantung pada apakah mereka berhubungan dengan field struct-nya atau parameter dan nilai kembalian method-nya.

Nama lifetime buat field struct selalu harus dideklarasikan setelah keyword impl dan kemudian dipakai setelah nama struct-nya karena lifetimes itu adalah bagian dari tipe struct-nya.

Di dalam method signatures di dalam blok impl, referensi mungkin terikat ke lifetime dari referensi di dalam field struct, atau mungkin mereka independen. Selain itu, aturan lifetime elision sering kali bikin anotasi lifetime tidak diperlukan lagi di method signatures. Mari kita lihat beberapa contoh yang memakai struct bernama ImportantExcerpt yang kita definisikan di Listing 10-24.

Pertama kita bakal memakai sebuah method bernama level yang satu-satunya parameternya adalah referensi ke self dan nilai kembaliannya adalah sebuah i32, yang mana bukan referensi ke apa pun:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Deklarasi parameter lifetime setelah impl dan pemakaiannya setelah nama tipe itu wajib, tapi kita tidak diwajibkan buat menganotasi lifetime dari referensi ke self berkat aturan elision yang pertama.

Ini adalah contoh di mana aturan lifetime elision yang ketiga bisa diterapkan:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Ada dua input lifetimes, jadi Rust menerapkan aturan lifetime elision pertama dan memberi baik &self maupun announcement lifetimes mereka masing-masing. Lalu, karena salah satu parameternya adalah &self, tipe kembaliannya bakal dapat lifetime dari &self, dan semua lifetimes sudah lengkap terjelaskan.

Lifetime Static

Ada satu lifetime spesial yang perlu kita bahas yaitu 'static, yang menandakan kalau referensi yang bersangkutan bisa hidup selama keseluruhan durasi dari program. Semua string literals punya lifetime 'static, yang bisa kita anotasi seperti berikut:

#![allow(unused)]
fn main() {
let s: &'static str = "Saya punya lifetime static.";
}

Teks dari string ini disimpan langsung di dalam binary program kita, yang mana bakal selalu tersedia. Maka dari itu, lifetime dari semua string literals adalah 'static.

Kita mungkin bakal melihat saran di pesan error untuk memakai lifetime 'static. Tapi sebelum menentukan 'static sebagai lifetime buat sebuah referensi, pikirkan dulu apakah referensi yang kita punya itu sebenarnya hidup selama keseluruhan lifetime program kita atau tidak, dan apakah kita emang maunya begitu. Sebagian besar waktu, pesan error yang menyarankan lifetime 'static itu terjadi gara-gara kita mencoba membuat dangling reference atau ada ketidakcocokan (mismatch) antara lifetimes yang tersedia. Di kasus seperti itu, solusinya adalah dengan memperbaiki masalah utamanya, bukannya asal menentukan lifetime 'static.

Parameter Tipe Generik, Trait Bounds, dan Lifetimes Secara Bersamaan

Mari kita lihat secara singkat sintaks buat menentukan parameter tipe generik, trait bounds, dan lifetimes semuanya di satu fungsi!

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

Ini adalah fungsi longest dari Listing 10-21 yang mengembalikan string slice yang lebih panjang dari antara dua string slices. Tapi sekarang fungsi ini punya parameter ekstra bernama ann dari tipe generik T, yang mana bisa diisi oleh tipe apa pun yang mengimplementasikan trait Display seperti yang ditentukan sama klausa where. Parameter ekstra ini bakal dicetak memakai {}, itulah kenapa kita butuh trait bound Display. Karena lifetimes itu adalah salah satu tipe dari generik, deklarasi parameter lifetime 'a dan parameter tipe generik T berada di dalam satu daftar yang sama di dalam kurung sudut setelah nama fungsinya.

Ringkasan

Kita sudah ngebahas banyak hal di bab ini! Sekarang setelah kita paham tentang parameter tipe generik, traits dan trait bounds, serta parameter lifetime generik, kita udah siap buat nulis kode tanpa pengulangan yang bisa jalan di berbagai situasi yang beda-beda. Parameter tipe generik ngasih kita kemampuan buat menerapkan kode ke tipe yang berbeda. Traits dan trait bounds memastikan kalau walaupun tipe-tipenya generik, mereka tetap bakal punya perilaku yang dibutuhin sama kode kita. Kita udah belajar gimana cara memakai anotasi lifetime buat memastikan kalau kode fleksibel ini tidak bakal punya dangling references (referensi yang menggantung). Dan semua analisis ini terjadi saat compile time, yang sama sekali tidak memengaruhi performa saat runtime!

Percaya atau tidak, masih banyak lagi yang bisa dipelajari soal topik-topik yang kita bahas di bab ini: Bab 18 bakal ngebahas trait objects, yang merupakan cara lain buat memakai traits. Ada juga skenario-skenario yang lebih rumit yang melibatkan anotasi lifetime yang cuma bakal kita perlukan di situasi yang sangat tingkat lanjut (advanced); buat itu, kita bisa membaca Rust Reference. Tapi buat langkah selanjutnya, kita bakal belajar gimana cara menulis tests di Rust supaya kita bisa memastikan kalau kode kita berjalan persis seperti yang seharusnya.