Layar aplikasi sungguhan jarang berisi satu jenis komponen: tombol Button, kolom teks TextBox, dan gambar hidup berdampingan dalam satu tampilan. Di sinilah polimorfisme bertemu koleksi — dan juga tempat kekakuan Vec<T> pertama kali terasa. Aturan koleksi di Rust menyatakan setiap elemen Vec harus bertipe sama dengan ukuran sama pula, sementara Button dan TextBox adalah dua tipe berbeda dengan ukuran berbeda. Kombinasi Vec<Box<dyn Trait>> menyelesaikan kebuntuan tersebut: setiap elemen berupa fat pointer 16 byte yang seragam, sementara data konkrit yang beraneka ragam bersembunyi di heap dengan vtable masing-masing. Chapter ini membangun koleksi polymorphic selangkah demi selangkah — dari struct layar utuh, penambahan dan pengambilan elemen saat runtime, hingga versi pinjaman dengan Vec<&dyn Trait>.

Masalah: Vec Menuntut Satu Tipe Elemen #

Vec<T> menyimpan elemen-elemen secara berdampingan di heap. Agar indeks ke-sekian bisa dihitung dengan perkalian sederhana indeks * ukuran_elemen, semua elemen harus bertipe yang sama — Vec<Button> hanya menerima Button, dan upaya push dengan TextBox ditolak sebagai mismatched types. Generics tidak menolong di sini: Vec<T> untuk satu T tetap homogen, dan kita tidak bisa menulis Vec<Draw> karena Draw hanyalah trait tanpa ukuran — kelanjutan dari larangan Sized yang kita temui di Chapter Trait Object dyn.

Solusinya menumpuk dua mekanisme yang sudah kita kuasai. Box<dyn Draw> memberi setiap komponen wadah berukuran tetap 16 byte berisi pointer data dan pointer vtable. Vec<Box<dyn Draw>> kemudian cukup menyimpan barisan wadah seragam tersebut — dari sudut pandang Vec, semua elemen identik; dari sudut pandang runtime, setiap elemen bisa jadi tipe konkrit apa pun yang mengimplementasikan Draw.

Layar Berisi Komponen Campuran #

Mari kita rakit layar login sederhana dengan tiga komponen dari dua tipe berbeda:

rust
trait Draw {
    fn draw(&self);
}

struct Button {
    label: String,
}

struct TextBox {
    placeholder: String,
}

impl Draw for Button {
    fn draw(&self) {
        println!("Menggambar tombol [{}]", self.label);
    }
}

impl Draw for TextBox {
    fn draw(&self) {
        println!("Menggambar kolom teks ('{}')", self.placeholder);
    }
}

struct Screen {
    components: Vec<Box<dyn Draw>>,
}

impl Screen {
    fn render(&self) {
        for component in &self.components {
            component.draw();
        }
    }
}

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(Button { label: String::from("Simpan") }),
            Box::new(TextBox { placeholder: String::from("Nama lengkap") }),
            Box::new(Button { label: String::from("Batal") }),
        ],
    };

    screen.render();
}

Output yang Dihasilkan:

text
Menggambar tombol [Simpan]
Menggambar kolom teks ('Nama lengkap')
Menggambar tombol [Batal]

Satu perulangan menggambar ketiga komponen tanpa peduli tipe konkritnya. Mari kita bedah kode di atas:

  1. Baris 25-27 (struct Screen { components: Vec<Box<dyn Draw>> }): field components memuat daftar polymorphic. Screen tetap Sized karena Vec apa pun isinya selalu berukuran 24 byte — tiga word untuk pointer buffer, panjang, dan kapasitas.
  2. Baris 29-35 (impl Screen { fn render(&self) ... }): method render meminjam seluruh isi layar lewat &self, lalu menelusuri setiap elemen dengan reference. Pemanggilan component.draw() pada tiap iterasi adalah dynamic dispatch — tiap elemen memilih versi draw milik vtable-nya sendiri.
  3. Baris 39-43 (components: vec![...]): makro vec! membangun tiga Box<dyn Draw> sekaligus. Dua tombol dan satu kolom teks — tipe konkrit berbeda — duduk rapi dalam satu koleksi karena wadahnya seragam.
  4. Baris 40 dan 42 (Box::new(Button { ... })): kedua tombol memakai tipe yang sama tetapi tetap menjadi dua nilai berbeda di heap. Tidak ada deduplikasi; setiap Box::new melakukan alokasi heap barunya sendiri.

Urutan output mengikuti urutan penyimpanan: Vec menjaga tertib posisi, sehingga layar digambar dari komponen teratas sampai terbawah persis seperti daftar yang kita tulis.

Menambah dan Mengambil Komponen Saat Runtime #

Koleksi statis dengan vec! hanya setengah cerita — kekuatan sesungguhnya muncul ketika isi layar ditentukan oleh kejadian runtime. Program berikut memilih komponen berdasarkan variabel choice, menambahkannya ke Vec yang masih kosong, lalu mengambil kembali elemen pertama:

rust
trait Draw {
    fn draw(&self);
}

struct Button {
    label: String,
}

struct TextBox {
    placeholder: String,
}

impl Draw for Button {
    fn draw(&self) {
        println!("Menggambar tombol [{}]", self.label);
    }
}

impl Draw for TextBox {
    fn draw(&self) {
        println!("Menggambar kolom teks ('{}')", self.placeholder);
    }
}

fn main() {
    let mut components: Vec<Box<dyn Draw>> = Vec::new();

    let choice = "tombol";
    if choice == "tombol" {
        components.push(Box::new(Button { label: String::from("Kirim") }));
    } else {
        components.push(Box::new(TextBox { placeholder: String::from("Email") }));
    }

    components.push(Box::new(TextBox { placeholder: String::from("Kata sandi") }));

    println!("== Sebelum dihapus ==");
    for component in &components {
        component.draw();
    }

    let removed = components.remove(0);
    println!("== Komponen diambil dari layar ==");
    removed.draw();
    println!("Sisa komponen: {}", components.len());
}

Output yang Dihasilkan:

text
== Sebelum dihapus ==
Menggambar tombol [Kirim]
Menggambar kolom teks ('Kata sandi')
== Komponen diambil dari layar ==
Menggambar tombol [Kirim]
Sisa komponen: 1

Mari kita bedah kode di atas:

  1. Baris 26 (let mut components: Vec<Box<dyn Draw>> = Vec::new();): koleksi dimulai kosong dan bertipe pasti sejak awal — Vec dari Box<dyn Draw>. Kata mut membuka jalan bagi push dan remove.
  2. Baris 28-33 (if choice == "tombol" { ... } else { ... }): keputusan tipe diambil saat runtime. Dalam program nyata, choice bisa berasal dari file konfigurasi, input pengguna, atau respons jaringan — kompilasi tidak perlu diulang ketika keputusan berubah.
  3. Baris 30 dan 32 (components.push(Box::new(...))): setiap cabang mendorong komponen berbeda. Dari sisi Vec, keduanya hanyalah elemen bertipe Box<dyn Draw>, sehingga pencampuran dua tipe konkrit berlangsung tanpa hambatan.
  4. Baris 42 (let removed = components.remove(0);): remove mengambil kepemilikan elemen indeks 0 keluar dari koleksi — komponen tombol pindah dari layar ke variabel removed, bukan disalin. Vec menggeser elemen sisanya mengisi posisi kosong.
  5. Baris 44 (removed.draw()): komponen yang sudah keluar dari koleksi tetap hidup dan berfungsi penuh di variabel barunya, karena kepemilikan Box<dyn Draw> ikut berpindah. Ketika removed keluar scope di akhir main, barulah data tombol di heap dibebaskan.

Versi Pinjaman: Vec<&dyn Shape> #

Pola Box mengambil kepemilikan penuh, tetapi tidak semua skenario menuntutnya. Ketika nilai-nilai sumber sudah hidup di stack milik satu fungsi dan hanya ingin dikumpulkan sementara untuk dihitung, koleksi pinjaman Vec<&dyn Shape> lebih ringkas — tidak ada alokasi Box, cukup fat pointer yang menunjuk data yang sudah ada:

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 shapes: Vec<&dyn Shape> = vec![&circle, &rect];

    let mut total = 0.0;
    for shape in &shapes {
        total += shape.area();
    }

    println!("Total luas: {:.2}", total);
}

Output yang Dihasilkan:

text
Total luas: 173.93

Mari kita bedah kode di atas:

  1. Baris 27-28 (let circle = ... dan let rect = ...): kedua nilai dimiliki langsung oleh main dan hidup di scope yang sama — syarat utama pola pinjaman.
  2. Baris 30 (let shapes: Vec<&dyn Shape> = vec![&circle, &rect];): elemen koleksi adalah reference ke dyn Shape, bukan Box. Setiap &circle dan &rect di-coerce menjadi fat pointer pinjaman pada saat masuk koleksi.
  3. Baris 33-35 (for shape in &shapes): perulangan meminjam koleksi secara read-only. shape.area() menjumlahkan luas kedua bentuk lewat dynamic dispatch tanpa satu pun alokasi heap tambahan.
  4. Baris 37 (println!("Total luas: {:.2}", total)): hasil penjumlahan 153.93 + 20.00 tercetak sebagai 173.93 — bukti dua tipe konkrit berbeda diolah dalam satu penjumlahan yang sama.

Batasan pola pinjaman: pemilik asli wajib tetap hidup selama koleksi dipakai. Koleksi Vec<&dyn Shape> tidak boleh mengembalikan elemennya ke pemanggil lain yang masa hidupnya lebih panjang, karena reference yang dibawanya hanya sah selama circle dan rect masih hidup. Aturan peminjaman dari section Ownership menjaga hal ini secara otomatis.

Ringkasan #

Vec<Box<dyn Draw>> menampung komponen berbeda tipe dalam satu koleksi seragam: setiap elemen adalah fat pointer 16 byte, datanya tersebar di heap, dan dynamic dispatch memilih method yang tepat pada tiap iterasi. Isi koleksi dapat bertumbuh lewat push dan menyusut lewat remove berdasarkan keputusan runtime tanpa kompilasi ulang, serta setiap elemen yang diambil keluar membawa kepemilikan Box-nya sendiri. Versi pinjaman Vec<&dyn Shape> menghemat alokasi dengan syarat pemilik asli tetap hidup. Mesin ini mulus, tetapi ada satu pertanyaan klasik yang belum terjawab: apa bedanya dengan sekadar enum berisi varian Button dan TextBox? Chapter berikutnya mengadu keduanya secara adil.