Fungsi bukan satu-satunya penampung pinjaman di Rust — struct juga bisa menyimpan reference di field-nya. Bayangkan struct untuk menampung kutipan dari dokumen panjang: alih-alih menyalin teks kutipan ke String baru, struct cukup meminjam potongan dari dokumen sumbernya, persis seperti string slice yang meminjam dari String. Konsekuensinya: sebuah instance struct bisa hidup lama, sementara data yang dipinjam field-nya punya pemilik sendiri yang bisa mati lebih dulu. Borrow checker kembali butuh kepastian hubungan umur, dan kali ini kepastian itu harus tertulis pada definisi struct. Chapter ini membedah penolakan E0106 pada field reference, bentuk perbaikannya dengan lifetime parameter, lalu batas umur yang ditegakkan pada pemakaian struct.

Struct yang Ingin Meminjam Data #

Skenario kita sederhana: struct Excerpt menyimpan satu field part bertipe &str — potongan teks dari novel yang jauh lebih besar. Meminjam, bukan menyalin, adalah pilihan yang tepat secara semantik karena potongan memang milik dokumen sumber. Versi definisi pertama berikut adalah upaya naif yang tidak dapat dikompilasi:

rust
struct Excerpt {
    part: &str,
}

fn main() {
    let novel = String::from("Belajar Rust itu menyenangkan. Semangat!");
    let first = Excerpt {
        part: &novel[..7],
    };

    println!("Potongan: {}", first.part);
}

Pesan error kompilator:

text
error[E0106]: missing lifetime specifier
 --> main.rs:2:11
  |
2 |     part: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Excerpt<'a> {
2 ~     part: &'a str,
  |

error: aborting due to 1 previous error

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

Pesan E0106 yang sama dengan Chapter sebelumnya muncul, tetapi kali ini sasaran penolakannya adalah definisi field, bukan tanda tangan fungsi. Kompilator menuntut setiap reference pada definisi struct menyatakan lifetime-nya, dengan alasan yang sama seperti pada fungsi: definisi struct adalah kontrak. Bayangkan instance Excerpt dibuat di main, lalu dipakai bertahun-tahun — pemanggil mana pun yang menerima instance tersebut berhak tahu field-nya meminjam dari data yang umurnya bagaimana, tanpa harus menyelidiki dari mana instance itu dibuat. Field &str tanpa lifetime tidak membawa informasi tersebut, sehingga kompilator menolaknya dan menyodorkan bentuk lengkap berikut lifetime <'a>.

Mendeklarasikan Lifetime pada Field #

Bentuk perbaikannya menjadikan Excerpt sebagai struct yang generic atas lifetime — persis pola Excerpt<'a> yang diusulkan pesan error. Semua pemakaian nama struct selanjutnya akan menyertakan parameter umur tersebut, sama seperti struct generic atas tipe yang selalu ditulis Nama<T>. Berikut versi yang sah:

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

fn main() {
    let novel = String::from("Belajar Rust itu menyenangkan. Semangat!");
    let first = Excerpt {
        part: &novel[..7],
    };

    println!("Potongan: {}", first.part);
}

Output yang Dihasilkan:

text
Potongan: Belajar

Mari kita bedah kode di atas:

  1. Baris 1 (struct Excerpt<'a>): deklarasi lifetime parameter a pada struct. Satu definisi kini berlaku untuk semua kemungkinan umur pinjaman — Excerpt tidak lagi satu tipe tunggal, melainkan keluarga tipe Excerpt<'a> untuk setiap a.
  2. Baris 2 (part: &'a str): field part menyatakan bahwa reference yang disimpannya berumur a. Inilah kontraknya: instance yang menyimpan field ini berjanji hanya hidup selama data yang dipinjamnya masih hidup.
  3. Baris 5–6 (let novel = ... dan let first = Excerpt { ... }): pada saat konstruksi, kompilator mengisi a dengan umur konkret — yaitu umur pinjaman &novel[..7] di titik ini. Kita boleh membayangkan first bertipe Excerpt<'umur-novel>, sama seperti generic type diisi tipe konkret saat dipakai.
  4. Baris 11 (println!("Potongan: {}", first.part);): pemakaian terjadi ketika novel masih hidup di scope main — kontrak terpenuhi tanpa kejutan.

Makna praktisnya mudah diingat sebagai satu kalimat: umur instance tidak boleh melampaui umur data yang dipinjam field-nya. Semua nama variabel, tipe kembalian fungsi, dan field struct lain yang memakai Excerpt akan ikut membawa parameter <'a> — kontrak umur menular ke seluruh kode yang bersinggungan dengan struct tersebut.

Batas Umur yang Ditegakkan #

Kontrak pada definisi struct tidak berhenti sebagai tulisan — borrow checker menegakkannya pada setiap konstruksi dan pemakaian. Buktinya adalah program berikut, yang membuat Excerpt meminjam data berumur pendek lalu mencoba memakai instance-nya setelah pemilik data mati. Kode ini tidak dapat dikompilasi:

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

fn main() {
    let first;

    {
        let novel = String::from("Belajar Rust itu menyenangkan. Semangat!");
        first = Excerpt {
            part: &novel[..7],
        };
    }

    println!("Potongan: {}", first.part);
}

Pesan error kompilator:

text
error[E0597]: `novel` does not live long enough
  --> main.rs:11:20
   |
 9 |         let novel = String::from("Belajar Rust itu menyenangkan. Semangat!");
   |             ----- binding `novel` declared here
10 |         first = Excerpt {
11 |             part: &novel[..7],
   |                    ^^^^^ borrowed value does not live long enough
12 |         };
13 |     }
   | - `novel` dropped here while still borrowed
14 |
15 |     println!("Potongan: {}", first.part);
   |                              ---------- borrow later used here

error: aborting due to 1 previous error

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

Alur pelanggarannya sama dengan Chapter sebelumnya, hanya kini pinjaman disimpan di dalam struct: baris 9–11 memperlihatkan novel hidup di blok dalam dan dipinjam oleh field part, baris 13 mencatat novel di-drop saat blok ditutup, dan baris 15 menunjukkan pemakaian first.part setelahnya. Definisi Excerpt<'a> mengikat umur instance ke umur pinjaman; karena pinjaman lahir dari novel, instance first tidak boleh hidup melampaui novel — dan println! di scope luar melanggar batas itu.

Dua perbaikan yang sah sama-sama bekerja dengan menggeser urutan dua garis waktu, bukan dengan menyentuh definisi struct: pindahkan println! ke dalam blok agar pemakaian instance selesai sebelum novel di-drop, atau naikkan novel ke scope luar agar umur pemiliknya mengikuti umur instance yang meminjamnya. Mendeklarasikan ulang struct atau menukar parameter lifetime tidak akan pernah menjadi solusi — kontrak <'a> sudah benar sejak awal; yang keliru hanyalah urutan umur pada titik pemakaian.

Meminjam atau Memiliki #

Kapan struct sebaiknya meminjam lewat field reference, dan kapan sebaiknya memiliki datanya sendiri? Field bertipe String atau Vec<T> membuat struct mandiri: mudah dibuat, mudah dipindahkan, dan tanda tangan semua fungsi yang menyentuhnya bersih dari <'a>. Field bertipe &'a str menghemat alokasi dan penyalinan — struct hanya menyimpan penunjuk dan panjang — serta cocok ketika struct memang semantiknya jendela ke data lain: parser yang membaca masukan, tampilan atas dokumen yang di-cache, atau kutipan atas teks sumber seperti Excerpt kita. Harganya jelas terlihat di chapter ini: parameter lifetime menular ke seluruh pengguna struct, dan setiap pemakaian terikat batas umur pemilik data.

Ringkasan #

Struct yang menyimpan reference wajib mendeklarasikan lifetime parameter: struct Excerpt<'a> { part: &'a str } menyatakan bahwa instance hanya boleh hidup selama data yang dipinjam field-nya masih valid. Definisi tanpa lifetime ditolak sebagai E0106 dengan usulan bentuk lengkap, dan pemakaian yang melampaui umur pemilik data ditolak sebagai E0597 — kontrak ditegakkan dari definisi hingga titik pemakaian terakhir. Konstruktor, variabel, dan fungsi yang bersinggungan dengan struct generic lifetime semuanya membawa parameter <'a> sebagai bagian dari tanda tangannya. Berikutnya kita melihat kabar baik yang menunggu: sebagian besar tanda tangan yang kita tulis sehari-hari justru tidak perlu anotasi sama sekali, karena aturan lifetime elision menuliskannya untuk kita.