dyn Trait memang lentur, tetapi tidak semua trait boleh duduk di baliknya. Begitu sebuah trait dipakai sebagai trait object, seluruh pemanggilan method harus bisa dilayani dari vtable: pemanggil hanya memegang pointer data tanpa tahu tipe konkrit, sehingga setiap slot vtable harus berbentuk alamat fungsi yang bisa dipanggil seragam untuk tipe mana pun. Syarat tersebut dikenal luas dengan istilah object safety — kompilator versi baru menyebutnya dyn compatibility — dan pelanggarannya menghasilkan error E0038 yang sangat khas. Chapter ini membedah tiga pelanggaran paling sering ditemui — method yang mengembalikan Self, method dengan generic parameter, dan trait yang mewarisi Sized — beserta jalan keluar resminya dari setiap kasus.

Syarat Dasar Object Safety #

Inti aturannya satu kalimat: semua method trait harus bisa dipanggil tanpa mengetahui tipe konkrit, cukup dengan pointer data dari fat pointer. Dari kalimat tersebut, tiga larangan turunan lahir secara alami. Pertama, method tidak boleh mengembalikan Self, karena Self berarti “tipe konkrit yang disembunyikan” — ukurannya tidak diketahui pemanggil. Kedua, method tidak boleh punya generic type parameter, karena vtable harus memuat alamat untuk setiap kombinasi parameter tersebut dan kombinasinya tak terbatas. Ketiga, trait tidak boleh mensyaratkan Self: Sized, karena syarat tersebut menuntut semua implementor berukuran pasti — bertentangan langsung dengan sifat dyn Trait yang tidak Sized.

Pelanggaran Pertama: Method Mengembalikan Self #

Kasus paling umum datang dari pola builder: method yang menghasilkan salinan baru dari nilainya sendiri, sehingga tanda tangannya wajar ditulis fn upgrade(&self) -> Self. Trait Vehicle berikut terlihat tak berbahaya, tetapi penggunaannya sebagai dyn Vehicle langsung ditolak:

rust
trait Vehicle {
    fn name(&self) -> String;
    fn upgrade(&self) -> Self;
}

struct Bicycle {
    speed: f64,
}

impl Vehicle for Bicycle {
    fn name(&self) -> String {
        String::from("Sepeda")
    }

    fn upgrade(&self) -> Self {
        Bicycle { speed: self.speed + 5.0 }
    }
}

fn main() {
    let vehicles: Vec<Box<dyn Vehicle>> = vec![Box::new(Bicycle { speed: 10.0 })];

    for vehicle in &vehicles {
        println!("{}", vehicle.name());
    }
}

Pesan error kompilator:

text
error[E0038]: the trait `Vehicle` is not dyn compatible
  --> main.rs:21:31
   |
21 |     let vehicles: Vec<Box<dyn Vehicle>> = vec![Box::new(Bicycle { speed: 10.0 })];
   |                               ^^^^^^^ `Vehicle` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> main.rs:3:26
   |
 1 | trait Vehicle {
   |       ------- this trait is not dyn compatible...
 2 |     fn name(&self) -> String;
 3 |     fn upgrade(&self) -> Self;
   |                          ^^^^ ...because method `upgrade` references the `Self` type in its return type
   = help: consider moving `upgrade` to another trait
   = help: only type `Bicycle` implements `Vehicle`; consider using it directly instead.

error: aborting due to 1 previous error

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

Pesan E0038 tersebut menunjuk dua lokasi sekaligus: baris tempat kita menulis Box<dyn Vehicle> sebagai sumber masalah, dan definisi upgrade sebagai akar masalahnya. Alasannya mekanis: pemanggil yang memegang Box<dyn Vehicle> hanya mengetahui pointer data. Ketika pemanggil memanggil upgrade(), kode mesin harus menyediakan ruang stack untuk nilai kembalian sebelum memanggil fungsi — dan ukuran ruang itu bergantung pada Self, yang berbeda untuk tiap tipe. Informasi ukuran tidak pernah hadir di fat pointer, sehingga pemanggilan semacam itu mustahil diterjemahkan.

Perbaikan: Kembalikan Box #

Solusi resminya sudah tersembunyi di saran kompilator: pindahkan upgrade ke trait lain, atau ubah tipe kembalinya menjadi Box<dyn Vehicle>. Opsi kedua mempertahankan satu trait sekaligus menjaga pola upgrade berantai — nilai baru tetap dikenal sebagai trait object, sehingga versi program berikut kompilasi mulus:

rust
trait Vehicle {
    fn name(&self) -> String;
    fn upgrade(&self) -> Box<dyn Vehicle>;
}

struct Bicycle {
    speed: f64,
}

impl Vehicle for Bicycle {
    fn name(&self) -> String {
        format!("Sepeda ({:.1} km/jam)", self.speed)
    }

    fn upgrade(&self) -> Box<dyn Vehicle> {
        Box::new(Bicycle { speed: self.speed + 5.0 })
    }
}

fn main() {
    let mut vehicle: Box<dyn Vehicle> = Box::new(Bicycle { speed: 10.0 });
    println!("Sebelum upgrade: {}", vehicle.name());

    vehicle = vehicle.upgrade();
    println!("Sesudah upgrade: {}", vehicle.name());
}

Output yang Dihasilkan:

text
Sebelum upgrade: Sepeda (10.0 km/jam)
Sesudah upgrade: Sepeda (15.0 km/jam)

Mari kita bedah perubahan kuncinya:

  1. Baris 3 (fn upgrade(&self) -> Box<dyn Vehicle>;): nilai kembalian berukuran pasti 16 byte — fat pointer — sehingga pemanggil tahu persis berapa ruang yang disiapkan. Rencana “kembalikan tipe konkrit” berubah menjadi “alokasikan di heap, serahkan kotaknya”.
  2. Baris 16 (Box::new(Bicycle { speed: self.speed + 5.0 })): implementor mengisi kotak dengan tipe konkrit pilihannya sendiri. Trait tetap tidak menyebut Bicycle; setiap implementor bebas mengembalikan tipe konkrit berbeda.
  3. Baris 24 (vehicle = vehicle.upgrade();): pemanggilan upgrade dari dalam trait object kini sah. Box lama di-drop dan Box baru mengambil tempatnya — pola pembaruan berantai tetap terasa seperti method biasa.

Pelanggaran Kedua: Method dengan Generic Parameter #

Pelanggaran kedua berasal dari method yang tampak praktis: satu method dengan generic parameter agar bisa dipakai untuk banyak tipe argumen. Trait Logger berikut ingin bisa mencatat pesan dengan tag apa pun yang bisa dicetak:

rust
trait Logger {
    fn log(&self, message: &str);
    fn log_tagged<T: std::fmt::Display>(&self, tag: T, message: &str);
}

struct Console;

impl Logger for Console {
    fn log(&self, message: &str) {
        println!("[LOG] {}", message);
    }

    fn log_tagged<T: std::fmt::Display>(&self, tag: T, message: &str) {
        println!("[{}] {}", tag, message);
    }
}

fn main() {
    let logger: Box<dyn Logger> = Box::new(Console);
    logger.log("Sistem penyiraman menyala");
}

Pesan error kompilator:

text
error[E0038]: the trait `Logger` is not dyn compatible
  --> main.rs:19:25
   |
19 |     let logger: Box<dyn Logger> = Box::new(Console);
   |                         ^^^^^^ `Logger` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> main.rs:3:8
   |
 1 | trait Logger {
   |       ------ this trait is not dyn compatible...
 2 |     fn log(&self, message: &str);
 3 |     fn log_tagged<T: std::fmt::Display>(&self, tag: T, message: &str);
   |        ^^^^^^^^^^ ...because method `log_tagged` has generic type parameters
   = help: consider moving `log_tagged` to another trait
   = help: only type `Console` implements `Logger`; consider using it directly instead.

error: aborting due to 1 previous error

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

Alasannya berbeda dari kasus Self. Vtable dibangun sekali per pasangan trait dan tipe konkrit, sementara log_tagged<T> sebenarnya adalah keluarga method — satu method untuk T = i32, satu lagi untuk T = char, dan seterusnya tanpa batas. Kompilator tidak akan pernah tahu kapan harus berhenti mengisi slot vtable, sehingga method generic dilarang masuk. Dua perbaikan yang umum: pindahkan log_tagged ke trait terpisah yang hanya dipakai lewat generics, atau ubah parameter tag menjadi tipe pasti seperti &str bila variasinya memang terbatas.

Pelanggaran Ketiga: Trait yang Mewarisi Sized #

Pelanggaran ketiga jarang disengaja, tetapi penting dikenali karena muncul dari bentuk yang tidak mencurigakan. Supertrait Sized pada deklarasi trait menyatakan bahwa setiap implementor wajib berukuran pasti — syarat yang tepat bertentangan dengan dyn Animal yang sengaja tidak berukuran:

rust
trait Animal: Sized {
    fn speak(&self) -> String;
}

struct Cat {
    name: String,
}

impl Animal for Cat {
    fn speak(&self) -> String {
        format!("{} mengeong: Meong~", self.name)
    }
}

fn main() {
    let animal: Box<dyn Animal> = Box::new(Cat { name: String::from("Miming") });
    println!("{}", animal.speak());
}

Pesan error kompilator:

text
error[E0038]: the trait `Animal` is not dyn compatible
  --> main.rs:16:25
   |
16 |     let animal: Box<dyn Animal> = Box::new(Cat { name: String::from("Miming") });
   |                         ^^^^^^ `Animal` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> main.rs:1:15
   |
 1 | trait Animal: Sized {
   |       ------  ^^^^^ ...because it requires `Self: Sized`
   |       |
   |       this trait is not dyn compatible...
   = help: only type `Cat` implements `Animal`; consider using it directly instead.

error: aborting due to 1 previous error

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

Pesan tersebut menjelaskan hubungan sebab-akibatnya secara gamblang: dyn Animal adalah tipe yang tidak Sized, sedangkan supertrait Sized menuntut setiap Self — termasuk dyn Animal itu sendiri — berukuran pasti. Dua tuntutan tersebut tidak mungkin dipenuhi bersamaan, sehingga Animal tidak akan pernah bisa menjadi trait object selama deklarasinya masih mewarisi Sized.

Pintu Darurat where Self: Sized #

Ada satu penanda resmi untuk kasus “trait ingin tetap object safe, tetapi satu method memang harus melanggar”: tambahkan where Self: Sized pada method yang bermasalah. Penanda tersebut mengeluarkan method dari jangkauan vtable — method menjadi non-dispatchable, hanya bisa dipanggil pada tipe konkrit:

rust
trait Vehicle {
    fn name(&self) -> String;
    fn upgrade(&self) -> Self
    where
        Self: Sized;
}

struct Bicycle {
    speed: f64,
}

impl Vehicle for Bicycle {
    fn name(&self) -> String {
        format!("Sepeda ({:.1} km/jam)", self.speed)
    }

    fn upgrade(&self) -> Self {
        Bicycle { speed: self.speed + 5.0 }
    }
}

fn main() {
    let vehicle: Box<dyn Vehicle> = Box::new(Bicycle { speed: 10.0 });
    println!("Kendaraan saat ini: {}", vehicle.name());
}

Output yang Dihasilkan:

text
Kendaraan saat ini: Sepeda (10.0 km/jam)

Mari kita bedah kode di atas:

  1. Baris 3-5 (fn upgrade(&self) -> Self where Self: Sized;): bound where Self: Sized menandai upgrade sebagai non-dispatchable. Trait Vehicle secara keseluruhan kini dyn compatible karena satu-satunya method yang dilayani vtable adalah name.
  2. Baris 23 (let vehicle: Box<dyn Vehicle> = ...): pembuatan trait object berjalan mulus — error E0038 dari versi sebelumnya lenyap.
  3. Baris 24 (vehicle.name()): pemanggilan name sah karena method tersebut tetap dispatchable melalui vtable.

Konsekuensinya perlu dinyatakan terus terang. Bila kita menambahkan baris vehicle = vehicle.upgrade(); setelah baris 24, kompilator menolak dengan pesan singkat: error: the 'upgrade' method cannot be invoked on a trait object. Method non-dispatchable hanya bisa dipanggil lewat tipe konkrit — misalnya pada variabel bertipe Bicycle — bukan lewat dyn Vehicle. Pola ini juga dipakai pustaka standar: beberapa method Iterator seperti map dan filter memakai where Self: Sized, sehingga trait Iterator tetap bisa dipakai sebagai trait object sementara method adaptor tersebut hanya bisa dipanggil lewat tipe konkrit.

Ringkasan #

Object safety — atau dyn compatibility dalam bahasa kompilator modern — mensyaratkan setiap method bisa dipanggil cukup dari pointer data. Tiga pelanggaran utamanya: method mengembalikan Self (ukuran kembalian tidak diketahui), method dengan generic parameter (vtable-nya tak terbatas), dan supertrait Sized (bertentangan dengan sifat dyn yang tidak berukuran). Ketiganya dilaporkan sebagai E0038 dengan penunjukan pasangan lokasi sumber-akar masalah. Perbaikan standarnya: ganti Self dengan Box<dyn Trait>, pindahkan method generic ke trait lain, atau tandai method dengan where Self: Sized sebagai pintu darurat. Dengan trait yang aman, kita siap melangkah ke pemanfaatan paling populer dari dyn Trait: menampung banyak tipe berbeda dalam satu koleksi.