Generics memberi kita polimorfisme saat kompilasi: satu fungsi fn luas<T: Shape>(bentuk: &T) bisa melayani semua tipe konkrit yang mengimplementasikan trait Shape, lalu kompilator menyalin fungsi tersebut untuk setiap tipe lewat proses monomorphization. Pendekatan tersebut cepat, tetapi menuntut satu syarat — tipe konkrit harus sudah pasti sebelum program dikompilasi. Nyatanya, banyak keputusan tipe baru terjadi saat program berjalan: pengguna memilih bentuk mana yang digambar, konfigurasi menentukan komponen UI mana yang dimuat, atau satu wadah data harus menampung campuran Button dan TextBox sekaligus. Rust menjawab kebutuhan tersebut dengan trait object — tipe yang ditulis dyn Trait dan berperan sebagai pembungkus runtime yang menyembunyikan tipe konkrit di balik trait. Chapter ini membedah cara kerja dyn Trait, struktur fat pointer yang menopangnya, serta alasan tipe tersebut tidak pernah berdiri sendiri tanpa pointer.

Masalah: Tipe yang Baru Diketahui Saat Runtime #

Mari kita mulai dari masalah nyata. Bayangkan program penggambar bentuk: pengguna memilih lingkaran atau persegi dari menu, lalu program menghitung luasnya. Kode kita ingin menampung pilihan pengguna dalam satu variabel shape, apa pun bentuk yang dipilih. Generic dan impl Trait tidak bisa membantu karena keduanya mengikat tipe pada saat kompilasi — begitu kita menulis let shape = &circle;, tipe variabel tersebut terkunci menjadi &Circle, sehingga pernyataan shape = &rect; pada baris berikutnya ditolak sebagai mismatched types. Kita juga tidak bisa memakai Shape sebagai tipe nilai secara langsung karena Shape hanyalah trait tanpa ukuran tetap — kompilator tidak tahu berapa byte yang harus disiapkan untuk Circle, Rect, atau implementor lain.

Yang kita butuhkan adalah tipe yang berkata: “nilai di balik reference ini mengimplementasikan Shape, tetapi tipe konkritnya tidak perlu diketahui sekarang”. Rust menyediakan tipe tersebut dengan penulisan dyn Shape, dan nilai yang terbungkus tipe tersebut disebut trait object.

dyn Shape Menyembunyikan Tipe Konkret #

Kata dyn merupakan singkatan dari dynamic. Tipe &dyn Shape dibaca “reference menuju suatu tipe yang mengimplementasikan Shape”. Ketika &Circle dikonversi menjadi &dyn Shape, kompilator melakukan type erasure — informasi tipe konkrit dihapus dari tipe statis, lalu dialihkan ke tabel khusus bernama vtable yang disiapkan untuk setiap pasangan trait dan tipe konkrit. Hasilnya, satu variabel bertipe &dyn Shape boleh memegang &Circle pada satu momen dan &Rect pada momen berikutnya tanpa deklarasi ulang, karena keduanya terbungkus dalam tipe yang sama.

Mari kita buktikan dalam program utuh:

rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rect {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.1415 * self.radius * self.radius
    }
}

impl Shape for Rect {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn main() {
    let circle = Circle { radius: 7.0 };
    let rect = Rect { width: 4.0, height: 5.0 };

    let mut shape: &dyn Shape = &circle;
    println!("Luas saat ini: {:.2}", shape.area());

    shape = &rect;
    println!("Luas saat ini: {:.2}", shape.area());
}

Output yang Dihasilkan:

text
Luas saat ini: 153.93
Luas saat ini: 20.00

Program tersebut membuktikan tiga hal sekaligus: variabel shape yang sama melayani dua tipe konkrit berbeda, pemanggilan shape.area() tetap berjalan benar untuk keduanya, dan bagian main sama sekali tidak menyebut nama Circle atau Rect setelah pembuatan nilai. Mari kita bedah kode di atas:

  1. Baris 1-3 (trait Shape { fn area(&self) -> f64; }): trait Shape hanya memiliki satu method dengan receiver &self — bentuk method inilah yang bisa dipanggil lewat trait object. Syarat lengkapnya kita bahas tuntas di Chapter Object Safety.
  2. Baris 30 (let mut shape: &dyn Shape = &circle;): variabel shape dideklarasikan bertipe reference ke dyn Shape. Ekspresi &circle yang aslinya bertipe &Circle otomatis dikonversi (disebut unsized coercion) menjadi &dyn Shape pada saat pengikatan. Kata kunci mut diperlukan karena isi shape akan dipasang ulang.
  3. Baris 31 (shape.area()): method dipanggil melalui trait object. Kompilator tidak mengetahui tipe konkrit di balik shape, jadi pemanggilan diselesaikan saat runtime lewat vtable — mekanisme yang kita bedah di Chapter Static vs Dynamic Dispatch.
  4. Baris 33 (shape = &rect;): variabel yang sama sekarang memegang reference ke Rect. Karena tipe statisnya tetap &dyn Shape, penugasan tersebut sah tanpa shadowing dan tanpa variabel baru.
  5. Baris 34 (shape.area()): pemanggilan yang sama kini menghasilkan luas persegi 4.0 * 5.0 — keputusan method mana yang dijalankan diambil dari vtable milik pasangan Shape-Rect.

Fat Pointer: Dua Alamat dalam Satu Nilai #

Reference biasa seperti &Circle hanya berisi satu alamat memori menuju data, sehingga ukurannya satu word — 8 byte pada sistem 64-bit — dan dijuluki thin pointer. Reference ke trait object harus membawa dua informasi sekaligus: alamat menuju data konkrit, dan alamat menuju vtable yang memuat daftar method milik pasangan trait dan tipe tersebut. Karena membawa dua alamat, reference semacam ini dijuluki fat pointer dengan ukuran dua word atau 16 byte. Kita bisa membuktikan angka tersebut langsung dari Rust:

rust
use std::mem::size_of;

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.1415 * self.radius * self.radius
    }
}

fn main() {
    println!("Ukuran &Circle    : {} byte", size_of::<&Circle>());
    println!("Ukuran &dyn Shape : {} byte", size_of::<&dyn Shape>());
}

Output yang Dihasilkan:

text
Ukuran &Circle    : 8 byte
Ukuran &dyn Shape : 16 byte

Mari kita bedah kode di atas:

  1. Baris 1 (use std::mem::size_of;): fungsi size_of::<T>() dari modul std::mem melaporkan ukuran tipe T dalam byte pada arsitektur tempat program dikompilasi.
  2. Baris 18 (size_of::<&Circle>()): menghasilkan 8 karena &Circle adalah thin pointer — satu alamat saja, dan ukuran data Circle yang ditunjuk tidak memengaruhi ukuran referensinya.
  3. Baris 19 (size_of::<&dyn Shape>()): menghasilkan 16 karena tipe tersebut adalah fat pointer yang memuat dua alamat: pointer data dan pointer vtable. Selisih 8 byte itulah harga satu bungkus informasi tipe runtime.

Struktur fat pointer dapat digambarkan sebagai berikut:

Diagram di atas menegaskan pembagian tugas kedua kata: kata pertama menunjuk data Circle tempat field radius berada, sedangkan kata kedua menunjuk vtable yang berisi alamat fungsi area() versi Circle. Saat shape dipindahkan ke &rect, kata pertama berganti menunjuk data Rect dan kata kedua berganti menunjuk vtable milik pasangan Shape-Rect.

Tipe dyn Shape Tidak Bisa Berdiri Sendiri #

Jika &dyn Shape terbukti berguna, wajar kita penasaran: bisakah tanda & dilepas agar variabel menyimpan dyn Shape secara langsung? Kompilator menjawab tegas tidak. Program berikut sengaja gagal:

rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.1415 * self.radius * self.radius
    }
}

fn main() {
    let shape: dyn Shape = Circle { radius: 7.0 };
    println!("Luas: {:.2}", shape.area());
}

Pesan error kompilator:

text
error[E0308]: mismatched types
  --> main.rs:16:28
   |
16 |     let shape: dyn Shape = Circle { radius: 7.0 };
   |                ---------   ^^^^^^^^^^^^^^^^^^^^^^ expected `dyn Shape`, found `Circle`
   |                |
   |                expected due to this
   |
   = note: expected trait object `dyn Shape`
                    found struct `Circle`
   = help: `Circle` implements `Shape` so you could box the found value and coerce it to the trait object `Box<dyn Shape>`, you will have to change the expected type as well

error[E0277]: the size for values of type `dyn Shape` cannot be known at compilation time
  --> main.rs:16:9
   |
16 |     let shape: dyn Shape = Circle { radius: 7.0 };
   |         ^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn Shape`
   = note: all local variables must have a statically known size
help: consider borrowing here
   |
16 |     let shape: &dyn Shape = Circle { radius: 7.0 };
   |                +

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.

Kompilator melaporkan dua masalah sekaligus pada baris yang sama. Error E0308 muncul karena tidak ada konversi dari nilai Circle menjadi tipe dyn Shape — penyerahan nilai utuh semacam itu tidak dikenal Rust, dan saran kompilator justru mengarah ke pembungkusan dengan Box. Error E0277 adalah inti persoalannya: dyn Shape tidak mengimplementasikan trait Sized, padahal setiap variabel lokal wajib memiliki ukuran yang sudah diketahui saat kompilasi supaya kompilator tahu berapa byte yang dialokasikan di stack.

Sized adalah marker trait bawaan yang diimplementasikan otomatis untuk semua tipe berukuran tetap — Circle, Rect, String, hingga Vec<T> semuanya Sized. Tipe dyn Shape sengaja tidak Sized karena ukuran nilainya bergantung pada tipe konkrit yang disembunyikan, dan informasi tersebut baru tersedia saat runtime. Konsekuensinya, dyn Shape hanya boleh hidup di balik indirection — pointer berukuran tetap yang menunjuknya.

Ringkasan #

Trait object dyn Shape menyembunyikan tipe konkrit di balik satu tipe runtime: variabel &dyn Shape boleh berpindah dari &circle ke &rect, setiap pemanggilan method diselesaikan lewat vtable, dan mekanisme tersebut dibayar dengan fat pointer 16 byte — pointer data ditambah pointer vtable — alih-alih thin pointer 8 byte. Upaya menyimpan dyn Shape tanpa pointer ditolak E0277 karena tipe tersebut tidak Sized. Pertanyaan berikutnya sudah terlanjur terbuka: bagaimana caranya memiliki trait object secara penuh, bukan sekadar meminjam? Jawabannya ada pada Box<dyn Trait> di Chapter selanjutnya.