Sepanjang section ini kita mengoleksi tiga alat untuk satu tujuan yang sama — membuat kode bekerja untuk banyak tipe tanpa duplikasi. Generik dengan trait bound menghadirkan polimorfisme tercepat lewat monomorphization. Trait object dyn Trait menghadirkan fleksibilitas tertinggi lewat type erasure dan vtable. Enum dengan match menghadirkan kepastian tertinggi lewat himpunan tipe tertutup. Ketiganya valid, ketiganya idiomatik, dan justru karena itu keputusan pemilihan kadang terasa membingungkan. Chapter penutup ini menyandingkan ketiganya pada satu masalah yang sama, merangkum perbandingannya dalam satu tabel keputusan, lalu menutup dengan mitos performa yang perlu kita luruskan sebelum menulis baris kode berikutnya.

Tiga Jalan Menuju Polimorfisme #

Mari kita rekap karakter masing-masing alat dalam tiga kalimat. Generik (fn f<T: Shape>(x: &T)) menyalin kode per tipe pada saat kompilasi — pemanggilan method diikat langsung, cepat, tetapi satu variabel atau koleksi hanya melayani satu tipe konkrit. Trait object (&dyn Shape atau Box<dyn Shape>) menyembunyikan tipe konkrit di balik fat pointer — keputusan method ditunda ke runtime lewat vtable, sehingga tipe boleh campur dan dipilih bebas, dibayar beberapa langkah ekstra per pemanggilan. Enum (ShapeKind + match) membatasi himpunan tipe sejak desain — setiap varian ditangani eksplisit, data hidup inline, dan penambahan varian diawasi kompilator lewat exhaustive checking.

Satu Masalah, Tiga Solusi #

Ujian yang adil adalah masalah yang sama diselesaikan ketiganya sekaligus: menghitung total luas satu lingkaran dan satu persegi. Program berikut memuat ketiga pendekatan dalam satu berkas:

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
    }
}

enum ShapeKind {
    Circle(Circle),
    Rect(Rect),
}

impl ShapeKind {
    fn area(&self) -> f64 {
        match self {
            ShapeKind::Circle(circle) => circle.area(),
            ShapeKind::Rect(rect) => rect.area(),
        }
    }
}

fn static_area<T: Shape>(shape: &T) -> f64 {
    shape.area()
}

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

    let total_generic = static_area(&circle) + static_area(&rect);

    let objects: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 7.0 }),
        Box::new(Rect { width: 4.0, height: 5.0 }),
    ];
    let mut total_dynamic = 0.0;
    for object in &objects {
        total_dynamic += object.area();
    }

    let kinds = vec![
        ShapeKind::Circle(Circle { radius: 7.0 }),
        ShapeKind::Rect(Rect { width: 4.0, height: 5.0 }),
    ];
    let mut total_enum = 0.0;
    for kind in &kinds {
        total_enum += kind.area();
    }

    println!("Total via generik : {:.2}", total_generic);
    println!("Total via dyn     : {:.2}", total_dynamic);
    println!("Total via enum    : {:.2}", total_enum);
}

Output yang Dihasilkan:

text
Total via generik : 173.93
Total via dyn     : 173.93
Total via enum    : 173.93

Ketiga jalur mendarat di angka yang sama — perbedaannya sepenuhnya di balik layar. Mari kita bedah kode di atas:

  1. Baris 40-42 (fn static_area<T: Shape>(shape: &T) -> f64): jalur generik. Dua pemanggilan di baris 48 memicu monomorphization sehingga kompilator menyiapkan salinan static_area versi Circle dan versi Rect, masing-masing dengan pemanggilan area yang diikat langsung.
  2. Baris 50-57 (let objects: Vec<Box<dyn Shape>> = ...): jalur trait object. Koleksi ini satu-satunya yang sah memuat dua tipe konkrit berbeda sekaligus — kemampuan yang tidak dimiliki dua jalur lain — dengan biaya satu alokasi heap per nilai dan dynamic dispatch pada setiap object.area().
  3. Baris 59-66 (let kinds = vec![...]): jalur enum. Nilai dibungkus varian ShapeKind saat masuk koleksi, match memilih implementasi luas yang tepat saat kompilasi, dan Circle maupun Rect disalin ke dalam enum tanpa menyentuh heap.
  4. Baris 68-70 (println!("Total via ...")): ketiga baris mencetak angka 173.93 — penjumlahan 153.93 dan 20.00 yang dihasilkan tiga jalur berbeda. Persamaan hasil inilah poin pentingnya: pemilihan pendekatan bukan soal benar-salah, melainkan soal sifat yang kita beli dengan tiap pilihan.

Tabel Perbandingan #

Rangkuman keputusan sehari-hari untuk ketiga pendekatan:

PendekatanKecepatanUkuran binerFleksibilitasKapan dipakai
Generik (<T: Trait>, impl Trait)Paling cepat: monomorphization, pemanggilan langsung, peluang inlining penuhPaling besar: satu salinan fungsi per tipe yang terpakaiRendah untuk kasus campuran: satu tipe konkrit per variabel atau koleksiTitik awal default; hot path; tipe pasti saat kompilasi
Trait object (&dyn Trait, Box<dyn Trait>)Ada biaya vtable per pemanggilan; inlining umumnya hilangPaling kecil: satu salinan fungsi untuk semua tipeTertinggi: tipe boleh campur dan dipilih saat runtimeKoleksi heterogen; tipe dari crate atau plugin lain; binary ramping
Enum + matchSetara generik: keputusan saat kompilasi, data inlineKecil bila varian sedikit; bertambah tiap lengan matchTertutup: menambah tipe berarti menambah varian dan merevisi semua matchHimpunan tipe tertutup dan stabil; protokol pesan; state machine

Baris pertama dan ketiga tabel terlihat mirip pada kolom kecepatan, dan memang keduanya sama-sama static dispatch. Pembedanya ada di kolom fleksibilitas: generik terbuka bagi tipe baru selama tipe tersebut mengimplementasikan trait, sedangkan enum menutup daftar tipe di dalam definisinya sendiri.

Alur Keputusan #

Proses memilih dapat dipetakan menjadi dua pertanyaan berurutan:

Alur tersebut bergerak dari pertanyaan paling menentukan: sifat himpunan tipe. Daftar yang tertutup — jumlahnya tetap, seluruhnya kita kendalikan — menempatkan enum sebagai kandidat terkuat karena exhaustive checking berubah dari beban menjadi perlindungan. Daftar yang terbuka melanjutkan ke pertanyaan kedua: apakah nilai-nilainya perlu hidup berdampingan atau dipilih belakangan? Kebutuhan koleksi campuran atau keputusan runtime menunjuk trait object; selebihnya, generik memberi abstraksi tanpa biaya runtime. Perlu dicatat bahwa keputusan ini bukan kontrak permanen — kode library sering memulai dari generik lalu menambah API Box<dyn Trait> begitu pengguna membutuhkan heterogenitas.

Mitos Performa dan Kebiasaan Baik #

Mitos yang paling sering beredar: dynamic dispatch selalu harus dihindari karena “lambat”. Kenyataannya satu pemanggilan lewat vtable hanya menambah beberapa nanodetik — pada kode aplikasi biasa, selisih tersebut tenggelam oleh biaya akses basis data, parsing JSON, atau sekali pun println!. Kode yang benar-benar sensitif terhadap selisih nanodetik adalah hot loop murni, dan di sanalah gambar berubah: tanpa inlining, kompilator kehilangan rentetan optimasi lanjutan, sehingga selisihnya bisa berlipat dari sekadar biaya lompatan vtable. Kesimpulan praktisnya dua arah: jangan menghindari dyn Trait karena mitos, dan jangan pula mengabaikan generics pada loop ketat yang terukur panas.

Kebiasaan baik penutup: perlakukan pemilihan pendekatan sebagai bagian dari desain API, bukan trik mikro. Generik mengundang pemanggil memakai tipe mereka sendiri; dyn Trait mengundang integrasi lewat trait object tanpa recompile; enum mengundang konsumsi menyeluruh lewat match. Pembaca kode kita akan mengikuti bentuk yang kita pilih, jadi pilih bentuk yang menceritakan maksudnya.

Ringkasan #

Ketiga alat polimorfisme menyelesaikan masalah yang sama dengan karakter berbeda: generik membeli kecepatan dengan salinan kode per tipe, trait object membeli fleksibilitas dengan vtable dan alokasi, enum membeli kepastian dengan himpunan tipe tertutup. Tabel perbandingan dan alur dua pertanyaan — tertutup atau tidak, heterogen atau tidak — menjadi kompas cepat untuk keputusan sehari-hari. Mitos “dynamic dispatch lambat” perlu diganti dengan kebiasaan mengukur sebelum menyimpulkan. Dengan selesainya section ini, kita memegang peta lengkap polimorfisme Rust — dan section berikutnya melangkah ke topik Smart Pointers, tempat Box yang sudah kita kenal akan bertemu saudara-saudaranya seperti Rc dan RefCell.