Lifetime Putaran Pertama

Kapan referensi hidup cukup lama beserta anotasi lifetime sederhana.

Daftar isi

Topik ini membahas lifetime, yaitu masa berlakunya sebuah referensi sejak dibuat sampai data yang ditunjuknya berhenti valid. Pembahasan mencakup cara borrow checker membandingkan masa hidup referensi dengan masa hidup datanya, anotasi 'a pada fungsi yang mengembalikan referensi, serta aturan elisi yang memungkinkan sebagian besar tanda tangan ditulis tanpa anotasi. Setelah membaca topik ini, error E0597 dapat dibaca sampai ke penyebabnya, dan fungsi yang mengembalikan referensi dapat dianotasi dengan benar tanpa menebak.

Masa hidup referensi dan borrow checker#

Setiap referensi memiliki masa hidup, yaitu wilayah program selama referensi itu dijamin menunjuk data yang masih valid. Borrow checker adalah pemeriksa pada kompilator yang membandingkan masa hidup setiap referensi dengan masa hidup data yang ditunjuknya, lalu menolak referensi yang berpeluang menggantung. Pemeriksaan ini bekerja pada wilayah program, bukan pada detik berjalannya program, sehingga seluruh keputusannya sudah selesai sebelum program dijalankan.

Pada variabel lokal, pemeriksaan tersebut berlangsung otomatis karena masa hidup referensi dan masa hidup data yang ditunjuknya dapat dibaca langsung dari lingkup blok. Referensi hanya boleh dibuat ke variabel yang lingkupnya mencakup seluruh pemakaian referensi itu, dan pemeriksaannya cukup membandingkan dua kurung kurawal. Kesulitan baru muncul ketika referensi keluar dari satu fungsi dan masuk ke fungsi lain, karena masa hidup pemanggil tidak dapat dilihat dari dalam fungsi yang dipanggil. Fungsi yang sama dapat dipanggil dari banyak tempat dengan umur variabel yang berbeda-beda, sehingga tidak ada satu jawaban tunggal yang dapat ditanam ke dalam isi fungsinya. Pada titik itulah anotasi lifetime diperlukan, bukan untuk menghitung umur, melainkan untuk menyatakan syarat umur yang harus dipenuhi setiap pemanggil.

Masalah pada fungsi yang mengembalikan referensi#

Fungsi yang mengembalikan referensi mengandung satu ketidakjelasan yang tidak dapat diselesaikan kompilator sendirian. Tanda tangan berikut tidak menyatakan referensi hasilnya menunjuk data milik parameter mana, padahal kedua parameter sama-sama layak menjadi sumbernya. Isi fungsi memang mengembalikan x atau y sesuai hasil perbandingan panjang, sehingga sumber hasilnya berganti mengikuti data yang masuk. Kompilator tidak menelusuri isi fungsi seperti itu untuk menebak tanda tangan.

rust Contoh: tanda tangan tanpa anotasi lifetime
// tidak-dijalankan
fn terpanjang(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

Tanpa informasi tambahan, borrow checker di sisi pemanggil tidak dapat memastikan hasil fungsi tetap aman dipakai, karena pemanggil tidak tahu referensi hasilnya menumpang pada umur parameter mana. Kompilator menolak tanda tangan seperti itu dengan error E0106 yang meminta lifetime specifier, bukan karena fungsi seperti itu mustahil, melainkan karena hubungan umur antara hasil dan parameternya harus dinyatakan secara eksplisit. Penolakan ini muncul saat kompilasi fungsi itu sendiri, bahkan sebelum ada pemanggil mana pun, sehingga perbaikannya juga berada pada tanda tangan dan bukan pada isi fungsinya.

Anotasi lifetime dengan ‘a#

Anotasi lifetime menegaskan hubungan umur antara referensi yang masuk dan referensi yang keluar dari sebuah fungsi. Sintaksnya menyerupai tipe generik: <'a> pada daftar generik fungsi menyatakan sebuah parameter masa hidup bernama a, lalu &'a str mengikat tipe dan masa hidupnya sekaligus. Pada fungsi terpanjang, kedua parameter dan nilai kembalian memakai 'a yang sama, yang berarti hasilnya hanya dijamin valid selama kedua parameternya masih valid.

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

fn main() {
    let teks_a = String::from("lama sekali");
    let hasil;
    {
        let teks_b = String::from("pendek");
        hasil = terpanjang(teks_a.as_str(), teks_b.as_str());
        println!("Terpanjang: {}", hasil);
    }
}
text Keluaran
Terpanjang: lama sekali

Keluaran tersebut timbul dari pemanggilan yang sah: hasil dipakai di dalam blok tempat teks_b masih hidup, sehingga jaminan 'a terpenuhi. Apabila yang lebih panjang adalah teks_b, hasil akan menunjuk data milik teks_b, dan anotasi 'a mengharuskan hasil tidak lolos dari blok itu. Nama a sendiri adalah nama biasa, sama seperti nama tipe generik T, sehingga 'teks atau 'b sama sahnya apabila lebih menjelaskan maksud kode.

Memakai hasil dengan batas umur nyata#

Pelanggaran atas jaminan 'a muncul sebagai error E0597 pada sisi pemanggil. Program berikut memakai referensi r setelah blok yang memelihara s ditutup, sehingga r berpeluang menunjuk data yang sudah dijatuhkan.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: referensi dipakai setelah pemiliknya di-drop
printf 'fn main() {\n    let r;\n    {\n        let s = String::from("sementara");\n        r = &s;\n    }\n    println!("r = {}", r);\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,11p'
text Keluaran
error[E0597]: `s` does not live long enough
 --> salah.rs:5:13
  |
4 |         let s = String::from("sementara");
  |             - binding `s` declared here
5 |         r = &s;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `s` dropped here while still borrowed
7 |     println!("r = {}", r);
  |                        - borrow later used here

Pesan tersebut memuat kronologi lengkapnya: binding 's' declared here menandai pemilik data, borrowed value does not live long enough menandai pembuatan pinjamannya, s dropped here while still borrowed menandai titik ketika data dijatuhkan, dan borrow later used here menandai pemakaian referensi yang melewati titik tersebut. Prinsipnya sama dengan jaminan 'a pada fungsi terpanjang: referensi tidak boleh berumur lebih panjang dari pemilik datanya. Perbaikannya ada pada urutan pemakaian, misalnya memindahkan pemakaian ke dalam blok sebagaimana pada berkas terpanjang.rs, atau memperpanjang umur datanya dengan memindahkan deklarasinya ke blok luar.

Kompilator tidak memilih salah satu jalur itu secara diam-diam karena keduanya mengubah makna kode: memakai data lebih awal dan memelihara data lebih lama adalah dua keputusan yang berbeda. Keputusan tersebut diserahkan kepada penulis kodenya, dan pesan errornya memetakan titik-titik yang menjadi kandidat perbaikan. Diagram berikut merangkum keadaan yang sah pada fungsi terpanjang.

let hasil = terpanjang(teks_a.as_str(), teks_b.as_str());teks_a (String) hidup sepanjang blok luarteks_b (String)hasil (&str)teks_b dibuatteks_b di-drophasil hanya sah selama teks_b masih hidup
Batas umur hasil fungsi terpanjang mengikuti umur data terpendek yang dipinjam

Aturan elisi: anotasi yang tidak perlu#

Sebagian besar tanda tangan tidak memerlukan anotasi karena kompilator menerapkan aturan elisi, yaitu penarikan kesimpulan masa hidup secara otomatis. Ada tiga aturan yang berlaku berurutan. Pertama, setiap parameter referensi mendapat masa hidupnya sendiri sebagai parameter implisit. Kedua, apabila fungsi memiliki tepat satu parameter masa hidup masukan, masa hidup itu diberikan ke seluruh referensi pada nilai kembaliannya. Ketiga, apabila fungsi adalah metode dengan &self atau &mut self, masa hidup self diberikan ke seluruh referensi pada nilai kembaliannya.

rust elisi.rs
fn potong_awal(s: &str, n: usize) -> &str {
    if n <= s.len() {
        &s[..n]
    } else {
        s
    }
}

fn main() {
    let teks = String::from("dokumentasi");

    println!("Potongan : {}", potong_awal(&teks, 4));
    println!("Utuh     : {}", potong_awal(&teks, 40));
}
text Keluaran
Potongan : doku
Utuh     : dokumentasi

Fungsi potong_awal memanfaatkan aturan kedua: satu-satunya parameter referensinya adalah s, sehingga nilai kembalian &str otomatis terikat pada masa hidup s, dan pemanggil dapat memastikan hasilnya valid selama teks masih hidup. Parameter n bertipe usize tidak ikut dihitung karena bukan referensi. Sebaliknya, fungsi terpanjang memiliki dua parameter referensi sehingga aturan kedua tidak berlaku, dan anotasi 'a menjadi wajib.

Kesalahan umum#

Kesalahan pertama adalah menganggap anotasi lifetime dapat memperpanjang umur data. Anotasi hanyalah pernyataan hubungan, sehingga menambah 'a pada tempat yang salah tidak akan pernah meloloskan referensi menggantung. Apabila error E0597 muncul, perbaikannya selalu mengubah kode, bukan anotasinya: pakai referensi selama datanya hidup, atau simpan data itu lebih lama dengan memperluas lingkup deklarasinya. Pesan errornya sendiri sudah memetakan jalan itu dengan empat penanda, mulai dari deklarasi pemilik sampai pemakaian referensi yang melampaui umurnya.

Kesalahan kedua adalah menambahkan 'a pada semua referensi hanya agar error E0106 hilang. Pemakaian 'a pada parameter dan hasil harus mencerminkan kenyataan pemakaian hasilnya. Parameter yang menjadi sumber hasil perlu memakai nama masa hidup yang sama dengan hasil, sedangkan parameter yang tidak pernah menjadi sumber hasil tidak perlu memakai nama itu. Apabila fungsi mengembalikan data yang dibuatnya sendiri, kembalikan tipe pemilik seperti String, bukan referensi yang dianotasi, karena data lokal tidak punya umur yang dapat dipinjam pemanggil.

Ringkasan#

  • Lifetime adalah masa berlakunya referensi, dan borrow checker memastikannya tidak melebihi masa hidup data yang ditunjuk.
  • Fungsi dengan dua parameter referensi yang mengembalikan referensi memerlukan anotasi <'a> untuk menyatakan hubungan umur antara masukan dan hasil.
  • Anotasi lifetime mendeskripsikan hubungan tanpa mengubah umur nilai, dan pelanggarannya di sisi pemanggil muncul sebagai error E0597.
  • Aturan elisi mengisi anotasi secara otomatis pada fungsi berparameter referensi tunggal dan pada metode yang meminjam self.

Referensi#