Lifetime Putaran Kedua

Struct yang menyimpan referensi beserta anotasi lifetime lengkap.

Daftar isi

Topik ini membahas lifetime pada definisi tipe, yaitu anotasi yang wajib menyertai struct yang menyimpan referensi. Pembahasan mencakup penolakan kompilator atas field referensi tanpa anotasi, penulisan parameter lifetime pada deklarasi struct dan blok impl, metode yang mengembalikan potongan dari field pinjaman, serta batas umur yang mengikat setiap instansi struct pada data yang dipinjamnya. Setelah membaca topik ini, struct yang menampung referensi dapat dianotasikan dengan benar, dan error E0597 pada pemakaiannya dapat dibaca sampai ke keputusan desain yang melatarbelakanginya.

Field referensi tanpa anotasi ditolak#

Sebuah struct yang field-nya bertipe referensi wajib menyatakan hubungan umur antara struct tersebut dan data yang ditunjuk field-nya. Syarat ini berbeda dari penalaran pada variabel lokal, yang masa hidupnya dapat dibaca langsung dari lingkup blok tempat variabel berada. Definisi struct berlaku untuk seluruh program dan tidak memiliki satu lingkup pemakaian tertentu, sehingga kompilator tidak dapat menebak umur mana yang dimaksud. Program kecil berikut menolak dikompilasi karena field teks hanya ditulis sebagai &str.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# struct dengan field referensi tanpa parameter lifetime
printf 'struct Penggalan {\n    teks: &str,\n}\n\nfn main() {\n    let novel = String::from("sensor suhu dipasang pagi");\n    let catatan = Penggalan { teks: &novel };\n    println!("{}", catatan.teks.len());\n}\n' > salah-anotasi.rs
# kompilasi ditolak karena field referensi memerlukan anotasi lifetime
rustc --edition=2021 salah-anotasi.rs 2>&1 | sed -n '1,11p'
text Keluaran
error[E0106]: missing lifetime specifier
 --> salah-anotasi.rs:2:11
  |
2 |     teks: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Penggalan<'a> {
2 ~     teks: &'a str,
  |

Pesan error menunjuk field teks dan meminta parameter lifetime bernama, lengkap dengan bentuk akhir yang disarankan pada blok bantuannya. Penolakan terjadi karena definisi struct harus menyatakan syarat umur secara umum, sedangkan pemeriksaan konkretnya berpindah ke setiap tempat yang membuat instansi. Posisi kegagalannya pun berbeda dari fungsi: error muncul pada deklarasi struct itu sendiri, sebelum ada satu pun instansi dibuat. Kelalaian semacam ini karenanya tidak pernah mencapai tahap menjalankan program. Bentuk &str tanpa anotasi memang tetap sah pada tanda tangan fungsi berkat aturan elisi, sehingga kebiasaan penulisan dari fungsi mudah terbawa ke deklarasi struct tanpa disadari.

Anotasi pada deklarasi struct dan blok impl#

Parameter lifetime pada struct ditulis seperti parameter tipe generik: <'a> sesudah nama struct, lalu &'a str pada field yang berupa referensi. Deklarasi tersebut tidak menetapkan satu umur tertentu, melainkan menyatakan bahwa setiap instansi membawa parameter 'a yang dipilih pada saat instansi dibuat. Blok impl wajib mengulang parameternya dengan impl<'a> Penggalan<'a>, karena parameter 'a pada deklarasi struct tidak otomatis terlihat di dalam blok implementasi. Lupa mengulangnya ditolak dengan error E0261 yang menyebut nama lifetime belum dideklarasikan.

rust penggalan.rs
struct Penggalan<'a> {
    teks: &'a str,
}

impl<'a> Penggalan<'a> {
    fn panjang(&self) -> usize {
        self.teks.len()
    }

    fn akhir(&self, mulai: usize) -> &'a str {
        &self.teks[mulai..]
    }
}

fn main() {
    let novel = String::from("sensor suhu dipasang pagi, kalibrasi selesai siang.");
    let catatan = Penggalan { teks: &novel[..20] };

    println!("teks    : {}", catatan.teks);
    println!("panjang : {}", catatan.panjang());
    println!("akhir   : {}", catatan.akhir(12));
}
text Keluaran
teks    : sensor suhu dipasang
panjang : 20
akhir   : dipasang

Irisan &novel[..20] meminjam dua puluh byte pertama dari novel, sehingga 'a pada instansi catatan terikat pada wilayah hidup novel. Metode panjang tidak membutuhkan anotasi apa pun karena satu-satunya referensi pada tanda tangannya adalah penerima &self, yang tidak perlu diberi nama berkat aturan elisi. Metode akhir memilih &'a str secara eksplisit: potongan yang dikembalikan berasal dari field teks, bukan dari penerima self, sehingga hasilnya tetap sah dipakai setelah pinjaman terhadap catatan berakhir. Pemilihan dua bentuk tersebut bukan gaya, melainkan pernyataan sumber data yang dikembalikan. Diagram berikut memperlihatkan hubungan antara instansi, field pinjaman, dan data pemiliknya.

struct Penggalan<'a> { teks: &'a str }novel (String) hidup sampai akhir fungsi maincatatan (Penggalan<'a>)teks: &'a strcatatan dibuatnovel di-dropwilayah 'a milik catatan harus berakhir sebelum novel dijatuhkan
Field teks pada Penggalan<'a> menunjuk data milik novel, dan wilayah 'a harus berada di dalam umur novel

Pemindai yang meminjam teks sumber#

Alasan paling lazim menyimpan referensi pada struct adalah pemrosesan tanpa penyalinan. Struct Pemindai berikut menyimpan potongan teks yang belum terbaca, lalu metode berikutnya memotong satu kata setiap kali dipanggil tanpa mengalokasikan String baru. Fungsi terasosiasi baru mengisi field sisa dengan teks sumber yang dipinjam pemanggilnya.

rust pemindai.rs
struct Pemindai<'a> {
    sisa: &'a str,
}

impl<'a> Pemindai<'a> {
    fn baru(teks: &'a str) -> Self {
        Pemindai { sisa: teks }
    }

    fn berikutnya(&mut self) -> Option<&'a str> {
        let sisa = self.sisa;
        if sisa.is_empty() {
            return None;
        }
        match sisa.find(char::is_whitespace) {
            Some(pos) => {
                let (kata, setelah) = sisa.split_at(pos);
                self.sisa = setelah.trim_start();
                Some(kata)
            }
            None => {
                self.sisa = "";
                Some(sisa)
            }
        }
    }
}

fn main() {
    let laporan = String::from("suhu 24.1 lembap 62");
    let mut pemindai = Pemindai::baru(&laporan);

    let mut kata: Vec<&str> = Vec::new();
    while let Some(potongan) = pemindai.berikutnya() {
        kata.push(potongan);
    }

    println!("jumlah kata : {}", kata.len());
    println!("hasil       : {:?}", kata);
    println!("sisa        : {:?}", pemindai.sisa);
}
text Keluaran
jumlah kata : 4
hasil       : ["suhu", "24.1", "lembap", "62"]
sisa        : ""

Nilai kembalian berikutnya sengaja dianotasikan Option<&'a str>, bukan Option<&str> polos. Dengan anotasi tersebut, setiap potongan kata terikat pada umur teks sumber, sehingga vektor kata dapat mengumpulkan potongan sambil pemindai terus dipanggil pada iterasi berikutnya. Apabila nilai kembalian ditulis polos, aturan elisi mengikatnya pada pinjaman &mut self. Akibatnya, pinjaman mutable terpaksa bertahan lintas iterasi, dan pengumpulan tersebut gagal dikompilasi dengan error E0499.

Di dalam metode, let sisa = self.sisa menyalin nilai penunjuknya ke variabel lokal terlebih dahulu, sehingga pembaruan self.sisa tidak mengganggu potongan yang sedang dikembalikan. Ketika penelusuran selesai, field sisa bernilai teks kosong dan seluruh potongan tetap menunjuk laporan yang masih hidup, bukan pemindai yang telah selesai dipakai. Seluruh proses berjalan tanpa satu byte pun teks yang disalin ulang.

Batas umur saat instansiasi#

Anotasi pada deklarasi struct tidak menjamin apa pun tentang data yang dipinjam pemanggilnya. Ketika instansi dibuat, 'a terikat pada wilayah hidup data sumbernya, dan pemakaian instansi di luar wilayah itu ditolak. Program berikut membuat Penggalan yang meminjam String lokal, lalu mencoba memakai instansi tersebut setelah blok pemilik data ditutup.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# instansi struct meminjam data yang hidupnya lebih pendek dari pemakaiannya
printf 'struct Penggalan<'\''a> {\n    teks: &'\''a str,\n}\n\nimpl<'\''a> Penggalan<'\''a> {\n    fn panjang(&self) -> usize {\n        self.teks.len()\n    }\n}\n\nfn main() {\n    let catatan;\n    {\n        let novel = String::from("sensor suhu dipasang pagi");\n        catatan = Penggalan { teks: &novel };\n    }\n    println!("panjang : {}", catatan.panjang());\n}\n' > salah-umur.rs
# kompilasi ditolak karena novel dijatuhkan sebelum pinjamannya selesai dipakai
rustc --edition=2021 salah-umur.rs 2>&1 | sed -n '1,11p'
text Keluaran
error[E0597]: `novel` does not live long enough
  --> salah-umur.rs:15:37
   |
14 |         let novel = String::from("sensor suhu dipasang pagi");
   |             ----- binding `novel` declared here
15 |         catatan = Penggalan { teks: &novel };
   |                                     ^^^^^^ borrowed value does not live long enough
16 |     }
   |     - `novel` dropped here while still borrowed
17 |     println!("panjang : {}", catatan.panjang());
   |                              ------- borrow later used here

Pesan error memetakan empat titik yang sama dengan kasus referensi biasa: deklarasi pemilik, pembuatan pinjaman, titik data dijatuhkan, dan pemakaian yang melampaui titik tersebut. Perbedaannya, pinjaman yang diperiksa kini tersimpan di dalam field struct, sehingga pemakaian catatan.panjang() pada baris terakhir menuntut novel tetap hidup selama catatan masih dipakai. Perbaikannya berada pada kode pemanggil, bukan pada anotasi: deklarasi novel dipindah ke blok luar agar umurnya mencakup seluruh pemakaian catatan, atau seluruh pemakaian catatan dipindah ke dalam blok tempat novel hidup. Kompilator tidak memilih salah satunya secara diam-diam karena kedua pilihan mengubah makna kode.

Kesalahan umum#

Mengikat semua field ke 'static demi menghilangkan error. Anotasi &'static str memaksa data bertahan sepanjang durasi program, sehingga error E0597 memang hilang, tetapi teks yang dibangun saat runtime tidak dapat memenuhinya tanpa kebocoran memori. Penulisan 'static adalah pernyataan bahwa data memang hidup selamanya, seperti literal teks yang tertanam pada berkas biner, bukan obat penawar error umur. Apabila data harus dibangun saat runtime, tipe pemilik seperti String menyelesaikannya tanpa mengubah umur nilai mana pun.

Menyimpan referensi padahal struktur berumur lebih panjang dari datanya. Kebutuhan meminjam muncul dari pola akses, dan bila struct ditujukan untuk hidup lama, kepemilikan berupa String biasanya menjadi desain yang lebih sederhana karena struct lepas dari umur pemanggilnya. Pindahkan ke field pinjaman hanya setelah alasan konkret muncul, misalnya biaya penyalinan yang terukur atau kontrak tampilan yang memang sementara. Error umur yang muncul berulang pada satu struct biasanya menandakan bahwa struct tersebut sebaiknya memiliki datanya sendiri.

Ringkasan#

  • Struct dengan field referensi wajib membawa parameter lifetime, dan penulisan tanpa anotasi ditolak dengan error E0106 pada deklarasi structnya.
  • Blok impl mengulang parameternya sebagai impl<'a> Tipe<'a>, dan kelalaian pengulangan ditolak dengan error E0261.
  • Metode boleh mengembalikan &'a str yang berasal dari field, sehingga hasilnya terikat pada umur data sumber, bukan pada umur pinjaman self.
  • Instansi struct yang meminjam data ditolak apabila hidup lebih lama dari datanya, dan pelanggarannya muncul sebagai error E0597 pada titik instansiasi.
  • Parameter lifetime dihapus sepenuhnya saat kompilasi, sehingga struct berreferensi tidak menambah biaya runtime.

Referensi#