Dua Chapter sebelumnya memperlihatkan keanehan yang menenangkan: Box menyimpan data di heap, tetapi kode kita mengakses isi layaknya variabel biasa — data.code, total_price(&receipt), bahkan println!("{}", angka) tanpa satu pun perintah pengekstrakan. Kegelisahan seharusnya muncul di sini: bagaimana bisa pointer dipakai seolah-olah pointer tersebut adalah nilainya sendiri? Jawabannya ada pada dua trait kecil yang menjadi tisu penghubung seluruh keluarga smart pointer: Deref untuk pembacaan dan DerefMut untuk perubahan. Keduanya menghidupkan mekanisme deref coercion — konversi reference otomatis yang berjalan berantai — dan memahami mekanisme ini adalah prasyarat sebelum kita melangkah ke Rc serta RefCell pada Chapter-Chapter berikutnya. Supaya mekanisminya tidak tinggal teori, kita akan membangun smart pointer sendiri bernama MyBox<T> dari nol.

Masalah: Smart Pointer yang Terasa Bukan Pointer #

Bayangkan Box<T> dirancang tanpa trait khusus: satu struct berisi pointer privat ke heap. Setiap pemakaian isinya menuntut pembongkaran manual — misalnya lewat method get() — sehingga kode yang natural seperti data.code = 110; berubah menjadi data.get().code = 110;, dan setiap pengiriman ke fungsi bertipe &ReportData menuntut penyalinan data.get() terlebih dahulu. Kode program yang mayoritas isinya pembungkusan dan pembongkaran pointer akan kehilangan kenyamanan justru di bagian yang paling sering ditulis.

Rust menuntut lebih dari sekadar kenyamanan: Box<String> harus bisa dikirim ke fungsi penerima &String tanpa perantara, dan MyBox<String> idealnya juga bisa menyusup ke fungsi penerima &str. Kedua kebutuhan itu — akses transparan dan konversi reference berantai — dipecahkan oleh satu trait yang sama: Deref.

Mekanisme Deref di Balik Layar #

Trait Deref hanya menuntut dua hal: associated type Target yang menyatakan tipe apa yang bisa ditunjuk, dan method deref(&self) -> &Target yang mengembalikan reference menuju isi. Dari kontrak sesederhana itu, kompilator membangun dua otomatisasi besar. Pertama, auto-deref pada pemanggilan method dan akses field: ketika method tidak ditemukan pada MyBox<String>, kompilator memanggil deref() lalu mencari method pada String, lalu pada str — rantai ini berlanjut sampai method ditemukan atau rantai habis. Kedua, ekspresi dereference *x pada smart pointer sebenarnya adalah gula sintaksis: *x ditulis ulang menjadi *(x.deref()) — panggil method deref(), lalu dereference reference yang dihasilkannya.

Dari sisi tipe, terdapat pula deref coercion: di lokasi yang tipe targetnya sudah diketahui — argument pemanggilan fungsi, nilai kembalian, atau pembanding — reference menuju smart pointer dikonversi otomatis mengikuti rantai Deref milik tipe tersebut: &MyBox<String> menjadi &String, lalu &String menjadi &str. Konversi ini berjalan satu arah dan hanya sebatangkai rantai yang dibutuhkan oleh tipe target.

Membuat MyBox dan Implementasi Deref #

Mari kita bangun smart pointer tiruan lengkap dengan trait Deref-nya:

rust
use std::ops::Deref;

struct MyBox<T> {
    value: T,
}

impl<T> MyBox<T> {
    fn new(value: T) -> MyBox<T> {
        MyBox { value }
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.value
    }
}

fn greet(name: &str) {
    println!("Halo, {}!", name);
}

fn main() {
    let boxed_name = MyBox::new(String::from("Budi Santoso"));
    let boxed_age = MyBox::new(17);

    greet(&boxed_name);
    println!("nama terdiri dari {} karakter", boxed_name.len());
    println!("usia: {}", *boxed_age);
}

Output yang Dihasilkan:

text
Halo, Budi Santoso!
nama terdiri dari 12 karakter
usia: 17

Mari kita bedah kode di atas:

  1. Baris 13 (impl<T> Deref for MyBox<T>): implementasi trait Deref untuk smart pointer buatan kita. Trait ini diimpor dari std::ops pada Baris 1.
  2. Baris 14 (type Target = T;): deklarasi tipe target — isi MyBox<T> memang bertipe T, sehingga dereference MyBox<String> akan menghasilkan String.
  3. Baris 16-18 (fn deref(&self) -> &T { … &self.value): jantung mekanismenya. Method deref hanya mengembalikan reference menuju field value — tanpa menyalin data, tanpa menghitung apa pun. Kontrak sederhana inilah yang dikonsumsi kompilator saat membangun auto-deref.
  4. Baris 29 (greet(&boxed_name);): aksi deref coercion yang paling spektakuler. Fungsi greet menuntut &str, pemanggil mengirim &MyBox<String>. Kompilator menjalankan rantai: &MyBox<String> → &String (lewat Deref milik MyBox) → &str (lewat Deref milik String). Dua konversi otomatis, nol perintah tambahan.
  5. Baris 30 (boxed_name.len()): method len tidak dimiliki MyBox, juga bukan milik String — len adalah method milik str. Auto-deref menuruni dua tingkat: MyBox<String> → String → str, lalu len() dipanggil pada potongan teks 12 karakter itu.
  6. Baris 31 (*boxed_age): bentuk eksplisit dereference, yang dibalik layar ditulis ulang menjadi *(boxed_age.deref()) — reference menuju angka 17 dihasilkan oleh method kita, lalu operator * membaca angkanya.

DerefMut: Deref untuk Nilai yang Berubah #

Pembacaan saja tidak cukup — Chapter Box Dasar menunjukkan *angka += 25 mengubah isi heap. Kemampuan itu lahir dari trait kembar DerefMut yang method-nya deref_mut(&mut self) -> &mut Target mengembalikan mutable reference. Aturannya ketat sekaligus adil: mendapatkan &mut T dari dalam smart pointer menuntut smart pointer itu sendiri sedang dipegang sebagai &mut, sehingga tidak ada jalan pintas untuk melanggar aturan borrowing:

rust
use std::ops::{Deref, DerefMut};

struct MyBox<T> {
    value: T,
}

impl<T> MyBox<T> {
    fn new(value: T) -> MyBox<T> {
        MyBox { value }
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.value
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.value
    }
}

fn main() {
    let mut boxed_score = MyBox::new(80);

    *boxed_score += 15;
    println!("nilai setelah ditambah: {}", *boxed_score);
}

Output yang Dihasilkan:

text
nilai setelah ditambah: 95

Mari kita bedah kode di atas:

  1. Baris 21 (impl<T> DerefMut for MyBox<T>): DerefMut hanya boleh diimplementasikan jika Deref juga terpasang dengan Target yang sama — keduanya selalu bekerja berpasangan.
  2. Baris 22 (fn deref_mut(&mut self) -> &mut T): penerima &mut self menegaskan syarat mutlaknya: hanya pemegang mutable reference atas smart pointer yang berhak menerima &mut T dari dalamnya.
  3. Baris 28 (let mut boxed_score = MyBox::new(80);): kata kunci mut pada variabel wajib ada — tanpa mut, Box tidak bisa dipegang sebagai &mut dan Baris 30 akan ditolak kompilator.
  4. Baris 30 (*boxed_score += 15;): ekspresi ini dibalik layar menjadi *(boxed_score.deref_mut()) += 15; — mutable reference menuju 80 diperoleh, angkanya dinaikkan menjadi 95 di tempat, lalu reference dilepas begitu statement berakhir.

Trait pasangan inilah yang membuat Box rasanya “hampa aksinya”: Box dari pustaka standar mengimplementasikan Deref dan DerefMut dengan Target = T, sehingga setiap perilaku yang kita rakit untuk MyBox di Chapter ini identik dengan perilaku Box asli pada dua Chapter sebelumnya.

Ringkasan #

Trait Deref menjadikan smart pointer transparan: method deref() mengembalikan reference menuju isi, kompilator memakainya untuk auto-deref berantai pada method dan field, deref coercion mengonversi &MyBox<String> menjadi &String hingga &str di setiap titik yang tipe targetnya diketahui, dan *x hanyalah gula sintaksis atas *(x.deref()). Trait kembar DerefMut membuka jalur perubahan lewat deref_mut() yang menuntut smart pointer dipegang sebagai &mut — pintu kecil yang menjaga aturan borrowing tetap utuh di dalam pointer. Dengan dua fondasi ini, kita siap memasuki wilayah smart pointer yang mengubah nasib nilai di heap: dimulai dari Rc yang memungkinkan satu data dimiliki banyak pemilik.