Di dunia C dan C++, ada satu kelas bug yang paling ditakuti: dangling pointer — penunjuk yang masih dipakai padahal memori yang ditunjuknya sudah dikembalikan ke sistem. Program bisa tetap berjalan beberapa saat, membaca sampah memori yang kebetulan menempati bekas alamat itu, lalu runtuh secara acak jauh dari sumber masalahnya — atau lebih buruk, membocorkan data yang seharusnya sudah terhapus. Rust menuntut agar kelas bug ini mustahil terjadi: setiap reference dijamin kompilator selalu menunjuk ke nilai yang masih hidup dan valid. Pada Chapter penutup section ini, kita akan mengadu langsung kompilator dengan dua usaha membuat referensi menggantung, membaca pesan penolakannya, lalu menemukan bentuk solusi yang benar.

Apa Itu Dangling Reference #

Dangling reference terjadi ketika sebuah reference masih beredar sementara nilai yang dipinjamnya sudah di-drop. Ingat kembali aturan ketiga ownership: nilai dijatuhkan saat pemiliknya keluar scope. Sekarang bayangkan seorang peminjam memegang alamat menuju sebuah teks, sementara pemilik teks itu keluar scope lebih dulu — heap sudah dipulangkan ke allocator, tetapi alamat bekas itu masih dipakai. Pembacaan melalui alamat tersebut sama saja dengan membaca isi gudang yang sudah dikosongkan dan disewakan ulang ke orang lain.

Kompilator Rust mencegah skenario ini dengan satu prinsip pemeriksaan: sebuah pinjaman tidak boleh hidup lebih lama dari pemiliknya. Prinsip ini diperiksa untuk setiap reference di setiap fungsi, dan pelanggarannya tidak pernah menjadi peringatan — selalu error kompilasi. Mari kita uji janji tersebut dengan dua eksperimen.

Kode yang Gagal: Referensi ke Nilai yang Sudah Di-drop #

Eksperimen pertama memakai scope blok. Kode berikut membuat reference di scope luar, tetapi nilai yang dipinjamnya hidup di scope dalam — kode ini tidak dapat dikompilasi:

rust
fn main() {
    let reference;

    {
        let message = String::from("Hanya sementara");
        reference = &message;
    }

    println!("Isi pesan: {}", reference);
}

Pesan error kompilator:

text
error[E0597]: `message` does not live long enough
 --> main.rs:6:21
  |
5 |         let message = String::from("Hanya sementara");
  |             ------- binding `message` declared here
6 |         reference = &message;
  |                     ^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `message` dropped here while still borrowed
8 |
9 |     println!("Isi pesan: {}", reference);
  |                               --------- borrow later used here

error: aborting due to 1 previous error

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

Pesan E0597 layak dibaca baris demi baris karena menceritakan kronologi lengkap bencana yang kita coba bangun:

  1. Baris 5–6 pada pesan (binding 'message' declared here dan borrowed value does not live long enough): message lahir di dalam scope dan dipinjam oleh reference pada baris 6.
  2. Baris 7 pada pesan ('`message` dropped here while still borrowed'): saat scope dalam ditutup, pemilik message keluar dan nilainya di-drop — padahal pinjamannya belum selesai. Titik inilah referensi mulai menggantung.
  3. Baris 9 pada pesan (borrow later used here): reference masih dipakai untuk mencetak setelah kejadian di atas. Karena ada pemakaian lanjutan, pinjaman dianggap masih hidup melewati kematian pemiliknya — pelanggaran.

Perhatikan bahwa tanpa pemakaian pada baris 9, kode justru akan diterima: reference yang tidak pernah dipakai lagi boleh mati sia-sia bersama data yang telah di-dropnya. Kompilator menghukum pemakaian atas nilai yang sudah tidak valid, bukan keberadaan variabelnya.

Kode yang Gagal: Mengembalikan Referensi dari Fungsi #

Eksperimen kedua memakai jalur return value, dan polanya paling sering menggoda pemula yang datang dari bahasa lain. Kode berikut mencoba mengembalikan reference ke nilai yang dibuat di dalam fungsi — juga tidak dapat dikompilasi:

rust
fn make_message() -> &String {
    let message = String::from("Halo dari dalam fungsi");
    &message
}

fn main() {
    let reference = make_message();
    println!("Isi pesan: {}", reference);
}

Pesan error kompilator:

text
error[E0106]: missing lifetime specifier
 --> main.rs:1:22
  |
1 | fn make_message() -> &String {
  |                      ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn make_message() -> &'static String {
  |                       +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn make_message() -> &String {
1 + fn make_message() -> String {
  |

error: aborting due to 1 previous error

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

Pesan E0106 ini istimewa: frasa bantuannya menyebut lifetime — mekanisme untuk mendeskripsikan berapa lama sebuah reference sah dipakai. Kita sengaja belum membahas lifetime di section ini karena topiknya menuntut konsep yang baru saja kita bangun; section Lifetime akan membahasnya tuntas. Yang perlu kita pahami sekarang adalah inti penolakannya, yang tertulis jelas pada baris bantuan: tipe kembalian memuat nilai pinjaman, tetapi tidak ada nilai yang bisa dipinjam — message akan di-drop begitu fungsi selesai, sehingga reference yang dikembalikan pasti menggantung sebelum sempat dipakai.

Kompilator bahkan menyodorkan dua usulan perbaikan. Usulan pertama (&'static) adalah niche yang jarang tepat; usulan kedua — ubah tipe kembalian menjadi String tanpa tanda & — adalah solusi yang benar untuk kasus ini, dan kebetulan tepat membawa kita ke bagian berikutnya.

Solusi: Kembalikan Nilai dengan Ownership #

Pembenahannya sederhana dan elegan: kembalikan nilai utuh, bukan reference-nya. Kepemilikan data ikut pindah ke pemanggil, sehingga data dijamin hidup selama pemanggil membutuhkannya.

rust
fn make_message() -> String {
    let message = String::from("Halo dari dalam fungsi");
    message
}

fn main() {
    let message_text = make_message();
    println!("Isi pesan: {}", message_text);
}

Output yang Dihasilkan:

text
Isi pesan: Halo dari dalam fungsi

Mari kita bedah solusinya:

  1. Baris 1 (fn make_message() -> String): tanda & pada tipe kembalian dihapus. Fungsi kini berjanji menyerahkan kepemilikan, bukan pinjaman — janji yang dapat dipenuhi.
  2. Baris 2 (let message = ...): teks dibuat di heap dan dimiliki oleh variabel lokal message, sama seperti versi gagal.
  3. Baris 3 (message): baris terakhir tanpa titik koma mengembalikan nilai tersebut. Karena tipe kembalian adalah String (bukan reference), yang berpindah adalah kepemilikan penuh — message tidak di-drop di dalam fungsi, melainkan terbang keluar bersama data heap-nya.
  4. Baris 7 (let message_text = make_message();): message_text di main menjadi pemilik baru. Data heap yang sama berpindah tangan tanpa disalin.
  5. Baris 8 (println!("Isi pesan: {}", message_text);): pencetakan aman karena pemilik nilai ini masih berada dalam scope — tidak ada lagi jendela waktu tempat data bisa mati lebih dulu.

Ringkasan Section Ownership dan Memory #

Kita telah menempuh perjalanan lengkap fondasi memori Rust. Stack dan heap memberi kita peta wilayah: data tetap di stack, data dinamis di heap dengan data pengelolanya di stack. Tiga aturan ownership menetapkan satu nilai satu pemilik pada satu waktu, dengan drop otomatis di ujung scope. Move semantics membuat penugasan dan pemanggilan fungsi menjadi serah terima kepemilikan, Copy dan Clone mengatur kapan penyalinan terjadi otomatis versus eksplisit, lalu referensi dan borrowing memungkinkan pemakaian data tanpa perpindahan kepemilikan — dengan aturan satu penulis atau banyak pembaca sebagai penjaga, dan jaminan bebas dangling reference sebagai penutup.

Seluruh jalinan ini bekerja pada saat kompilasi: tidak ada garbage collector, tidak ada penghitungan referensi pada jalur ini, dan tidak ada biaya runtime untuk pemeriksaannya. Dua pertanyaan besar menanti di depan: bagaimana mendeskripsikan umur reference secara eksplisit pada tanda tangan fungsi yang rumit (lifetime), dan bagaimana meminjam potongan data alih-alih keseluruhannya (slice) — keduanya menyusul di section Slice dan Referensi serta section Lifetime.