Reference &dyn Trait yang kita pakai di Chapter sebelumnya bersifat pinjaman: pemanggil hanya menitipkan alamat, dan trait object itu tetap milik orang lain. Pola tersebut runtuh begitu kita butuh kepemilikan — fungsi pabrik yang membuat objek secara internal tidak bisa mengembalikan reference ke nilai lokal karena nilai lokal tersebut mati saat fungsi usai, dan field struct yang ingin menampung komponen polymorphic juga butuh pemilik yang jelas. Untuk dua kebutuhan tersebut Rust menyiapkan Box<dyn Trait>: kombinasi Box yang mengalokasikan nilai di heap — persis seperti preview Box pada section Smart Pointers — dengan trait object yang menyembunyikan tipe konkrit. Chapter ini membedah cara kerja, pesan error khas, serta pola pemakaian Box<dyn Trait> pada nilai kembalian dan field struct.
Masalah: Trait Object yang Harus Dimiliki Sendiri #
Kita mulai dari masalah nyata. Fungsi make_animal menerima pilihan pengguna berupa teks, lalu membangun objek hewan yang sesuai — seekor Dog atau seekor Cat. Tipe dyn Animal yang dikembalikan tidak boleh dikirim apa adanya karena dyn Animal tidak Sized, seperti yang kita buktikan lewat error E0277 pada Chapter sebelumnya. Reference juga bukan jawaban: nilai Dog dibuat di dalam fungsi, jadi reference apa pun yang menunjuk nilai tersebut akan menggantung begitu fungsi berakhir dan stack frame-nya dibongkar.
Kebutuhannya jelas: fungsi harus memindahkan kepemilikan penuh kepada pemanggil, tetapi kepemilikan itu harus dibungkus tipe yang ukurannya sudah diketahui saat kompilasi. Box memenuhi kedua syarat tersebut sekaligus — ukuran Box<T> tidak pernah bergantung pada ukuran T, sementara data yang dikelolanya hidup di heap terlepas dari seberapa besar atau seberapa “tidak berukuran” tipe konkritnya.
Box: Kepemilikan Penuh di Heap #
Mekanismenya bekerja dalam dua langkah. Pertama, Box::new(Cat { ... }) mengalokasikan Cat di heap dan mengembalikan Box<Cat> — pointer biasa setebal satu word. Kedua, saat Box<Cat> diubah menjadi Box<dyn Animal>, kompilator menukar pointer tersebut menjadi fat pointer dua word: pointer menuju data di heap ditambah pointer menuju vtable. Setelah konversi itu, pemilik Box<dyn Animal> memegang kepemilikan data di heap; saat variabel pemilik keluar scope, Box di-drop dan memori heap ikut dibebaskan. Mari kita lihat pola lengkapnya:
trait Animal {
fn speak(&self) -> String;
}
struct Dog {
name: String,
}
struct Cat {
name: String,
}
impl Animal for Dog {
fn speak(&self) -> String {
format!("{} menggonggong: Guk guk!", self.name)
}
}
impl Animal for Cat {
fn speak(&self) -> String {
format!("{} mengeong: Meong~", self.name)
}
}
fn make_animal(choice: &str) -> Box<dyn Animal> {
if choice == "anjing" {
Box::new(Dog { name: String::from("Bobi") })
} else {
Box::new(Cat { name: String::from("Miming") })
}
}
fn main() {
let mut animal: Box<dyn Animal> = make_animal("kucing");
println!("{}", animal.speak());
animal = make_animal("anjing");
println!("{}", animal.speak());
}Output yang Dihasilkan:
Miming mengeong: Meong~
Bobi menggonggong: Guk guk!Mari kita bedah kode di atas:
- Baris 25 (
fn make_animal(choice: &str) -> Box<dyn Animal>): tipe nilai kembalianBox<dyn Animal>berjanji kepada pemanggil: “kotak ini berisi suatu hewan, tipe konkritnya rahasia”. UkuranBox<dyn Animal>yang tetap 16 byte membuat janji tersebut lolos syaratSized. - Baris 27 dan 29 (
Box::new(Dog { ... })/Box::new(Cat { ... })): kedua cabang sama-sama membangun nilai di heap lewatBox::new, lalu masing-masingBox<Dog>danBox<Cat>otomatis dikonversi menjadiBox<dyn Animal>— konversi dari pointer konkrit menuju trait object terjadi tanpa perintah tambahan. - Baris 34 (
let mut animal: Box<dyn Animal> = make_animal("kucing");): variabelanimalkini pemilik sah dataCatdi heap. Tidak ada yang dipinjam, jadianimalboleh dipindahkan, dikembalikan darimainbila perlu, atau disimpan dalam struct. - Baris 35 (
animal.speak()): pemanggilan method tetap berjalan lewat dynamic dispatch —Boxsecara otomatis melakukan dereference sehinggaanimal.speak()sama mudahnya dengan memanggil method pada nilai biasa. - Baris 37 (
animal = make_animal("anjing");): nilai kembalian yang baru menggantikan isianimal.Box<dyn Animal>lama di-drop lebih dulu — memoriCatdi heap dibebaskan — sebelumBoxbaru penampungDogmasuk.
Pola make_animal semacam ini dikenal sebagai factory function: satu titik pembuatan yang menyembunyikan detail tipe dari pemanggil. Setiap kali tipe hewan baru ditambahkan, pemanggil tidak perlu berubah satu baris pun.
Mengembalikan dyn Animal Tanpa Box Gagal Kompilasi #
Insting pertama banyak pemula adalah menulis tipe kembalian dyn Animal tanpa Box, karena terlihat lebih sederhana. Kompilator menolaknya dengan tiga laporan yang saling melengkapi:
trait Animal {
fn speak(&self) -> String;
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn speak(&self) -> String {
format!("{} mengeong: Meong~", self.name)
}
}
fn make_animal(name: &str) -> dyn Animal {
Cat { name: String::from(name) }
}
fn main() {
let animal = make_animal("Miming");
println!("{}", animal.speak());
}Pesan error kompilator:
error[E0746]: return type cannot be a trait object without pointer indirection
--> main.rs:15:31
|
15 | fn make_animal(name: &str) -> dyn Animal {
| ^^^^^^^^^^ doesn't have a size known at compile-time
|
help: consider returning an `impl Trait` instead of a `dyn Trait`
|
15 - fn make_animal(name: &str) -> dyn Animal {
15 + fn make_animal(name: &str) -> impl Animal {
|
help: alternatively, box the return type, and wrap all of the returned values in `Box::new`
|
15 ~ fn make_animal(name: &str) -> Box<dyn Animal> {
16 ~ Box::new(Cat { name: String::from(name) })
|
error[E0277]: the size for values of type `dyn Animal` cannot be known at compilation time
--> main.rs:20:9
|
20 | let animal = make_animal("Miming");
| ^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `dyn Animal`
= note: all local variables must have a statically known size
error[E0277]: the size for values of type `(dyn Animal + 'static)` cannot be known at compilation time
--> main.rs:20:18
|
20 | let animal = make_animal("Miming");
| ^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn Animal + 'static)`
= note: the return type of a function must have a statically known size
error: aborting due to 3 previous errors
Some errors have detailed explanations: E0277, E0746.
For more information about an error, try `rustc --explain E0277`.Error E0746 adalah berita utamanya: tipe kembalian tidak boleh berupa trait object tanpa pointer indirection. Dua saran yang menyertainya memetakan pilihan kita secara jujur — impl Animal jika semua cabang mengembalikan satu tipe yang sama, atau Box<dyn Animal> jika cabang-cabang mengembalikan tipe berbeda seperti kasus make_animal ini. Dua error E0277 yang mengikuti mengulang akar masalah dari Chapter sebelumnya: dyn Animal tidak Sized, sementara variabel lokal dan tipe kembalian wajib berukuran pasti saat kompilasi. Seluruh rangkaian error tersebut lenyap begitu Box::new membungkus nilai dan tipe kembalian diganti Box<dyn Animal>.
Menyimpan Trait Object di Field Struct #
Kebutuhan kedua kepemilikan muncul pada struct. Field widget: dyn Widget akan gagal dengan alasan yang sama — kompilator menolak karena “structs must have a statically known size to be initialized”, alias struct wajib berukuran pasti agar bisa dibuat nilainya. Field bertipe Box<dyn Widget> menyelesaikannya: struct hanya menyimpan fat pointer 16 byte, sementara komponen polymorphic yang sebenarnya hidup di heap:
trait Widget {
fn label(&self) -> String;
}
struct Button {
text: String,
}
struct TextBox {
placeholder: String,
}
impl Widget for Button {
fn label(&self) -> String {
format!("Tombol: {}", self.text)
}
}
impl Widget for TextBox {
fn label(&self) -> String {
format!("Kolom teks: {}", self.placeholder)
}
}
struct Panel {
title: String,
widget: Box<dyn Widget>,
}
fn main() {
let panel = Panel {
title: String::from("Panel Login"),
widget: Box::new(Button { text: String::from("Masuk") }),
};
println!("{} berisi '{}'", panel.title, panel.widget.label());
}Output yang Dihasilkan:
Panel Login berisi 'Tombol: Masuk'Mari kita bedah kode di atas:
- Baris 25-28 (
struct Panel { title: String, widget: Box<dyn Widget> }): dua field memberi ukuran pasti bagiPanel—Stringsebesar 24 byte ditambah fat pointer 16 byte. TipePanelmenjadiSized, dan setiapPanelboleh memilih widget berbeda tanpa mengubah definisi struct. - Baris 33 (
widget: Box::new(Button { ... })):Buttondibuat langsung di heap dan dikonversi menjadiBox<dyn Widget>pada pengisian field. Kita bisa mengganti baris tersebut denganTextBoxdan baris lain tidak berubah. - Baris 36 (
panel.widget.label()): akses method lewat dua lapis dereference — daripanelke fieldwidget, lalu dariBox<dyn Widget>ke dataButton— dan seluruh lapisan tersebut berjalan otomatis tanpa operator tambahan.
Ketika panel keluar scope di akhir main, urutan drop berjalan teratur: Panel dijatuhkan, Box<dyn Widget> ikut dijatuhkan, dan data Button di heap dibebaskan. Kepemilikan berlapis semacam ini persis seperti yang sudah kita kenal pada section Ownership.
Ringkasan #
Box<dyn Trait> menjadikan trait object sebagai nilai yang dimiliki penuh: Box::new mengalokasikan tipe konkrit di heap, konversi ke Box<dyn Trait> menukar pointer biasa menjadi fat pointer berisi pointer data dan pointer vtable, serta drop pada Box membebaskan memori heap secara otomatis. Tipe kembalian dyn Animal tanpa pointer ditolak E0746 lengkap dengan dua alternatif perbaikan, dan field struct mengikuti aturan yang sama demi ukuran struct yang pasti. Satu Box<dyn Trait> sudah menyelesaikan banyak kasus, tetapi layar sungguhan memuat belasan komponen berbeda sekaligus — di sanalah Vec<Box<dyn Trait>> berperan, bahan Chapter berikutnya.