Sebelum konsep trait object populer di dunia pemrograman, polimorfisme bertipe tertutup sudah lama dilayani oleh pola yang jauh lebih sederhana: enum yang memuat satu varian per tipe, plus satu method berisi match. Rust memelihara pola tersebut sampai hari ini karena enum punya kelebihan yang sulit ditiru trait object — kompilator menuntut setiap varian ditangani, data tersimpan inline tanpa heap, dan seluruh keputusan method terselesaikan saat kompilasi. Ketika daftar tipe kita memang tertutup dan sudah diketahui sejak awal — bentuk geometri dalam aplikasi gambar, jenis pesan dalam protokol, state mesin dalam game — enum layak jadi kandidat utama. Chapter ini membangun versi enum dari kasus Shape yang sejauh ini kita tangani dengan dyn, membedah jaminan exhaustive match-nya, lalu membandingkan jejak memorinya secara nyata.
Polimorfisme Tanpa Trait Object #
Pendekatannya lugas: satu enum Shape dengan varian per bentuk, lalu method area yang memilah lewat match. Tidak ada trait, tidak ada vtable, tidak ada Box:
enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => 3.1415 * radius * radius,
Shape::Rect { width, height } => width * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 7.0 },
Shape::Rect { width: 4.0, height: 5.0 },
];
for shape in &shapes {
println!("Luas: {:.2}", shape.area());
}
}Output yang Dihasilkan:
Luas: 153.93
Luas: 20.00Hasilnya identik dengan versi trait object, tetapi seluruh perjalanan kode berbeda. Mari kita bedah kode di atas:
- Baris 1-4 (
enum Shape { Circle { radius: f64 }, Rect { ... } }): enum menjadi wadah polimorfik. VarianCirclemembawa satu field, varianRectmembawa dua — perbedaan bentuk data antar tipe dikodekan sebagai perbedaan varian. - Baris 6-13 (
impl Shape { fn area(&self) -> f64 { match self { ... } } }): seluruh logika per-tipe terkumpul dalam satu method. Setiap lenganmatchmengikat field varian secara langsung —radiussiap dipakai tanpa dereference pointer. - Baris 16-19 (
let shapes = vec![...]):Vec<Shape>legal tanpaBoxkarena enum berukuran pasti. Kedua bentuk berbeda tipe internalnya, tetapi bagiVeckeduanya hanyalah elemen bertipeShape. - Baris 21-23 (
for shape in &shapes): pemanggilanshape.area()adalah static dispatch murni — kompilator melihat tipeShape, melihat method di blokimpl, dan menulis instruksi pemanggilan langsung beserta lompatanmatchuntuk memilih cabang.
Pola ini disebut polimorfisme tertutup: himpunan tipe dibekukan dalam definisi enum, dan setiap penambahan tipe berarti penambahan varian — tindakan yang disorot kompilator di seluruh titik penggunaan, seperti kita saksikan berikutnya.
Kompilator Menjaga Kelengkapan: Exhaustive Match #
Kekuatan terbesar enum bukan kecepatannya, melainkan jaminan kelengkapan. Ketika kita menambahkan varian Triangle dan lupa memperbarui area, program tidak berjalan setengah benar — program menolak dikompilasi:
enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => 3.1415 * radius * radius,
Shape::Rect { width, height } => width * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 7.0 },
Shape::Rect { width: 4.0, height: 5.0 },
Shape::Triangle { base: 6.0, height: 3.0 },
];
for shape in &shapes {
println!("Luas: {:.2}", shape.area());
}
}Pesan error kompilator:
error[E0004]: non-exhaustive patterns: `&Shape::Triangle { .. }` not covered
--> main.rs:9:15
|
9 | match self {
| ^^^^ pattern `&Shape::Triangle { .. }` not covered
|
note: `Shape` defined here
--> main.rs:1:6
|
1 | enum Shape {
| ^^^^^
...
4 | Triangle { base: f64, height: f64 },
| -------- not covered
= note: the matched value is of type `&Shape`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
11 ~ Shape::Rect { width, height } => width * height,
12 ~ &Shape::Triangle { .. } => todo!(),
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0004`.Error E0004 tersebut menunjuk dua lokasi: definisi enum sebagai pemilik varian baru, dan match di area sebagai titik yang belum diperbarui. Kompilator bahkan menuliskan kerangka lengan Triangle yang tinggal kita isi rumus luas segitiga — 0.5 * base * height menghasilkan 9.00 untuk contoh di atas. Jaminan semacam ini tidak dimiliki trait object: menambah tipe baru yang mengimplementasikan Draw tidak memicu pemeriksaan apa pun pada kode yang sudah ada, sehingga logika yang seharusnya diperbarui bisa lolos tanpa peringatan. Dengan enum, setiap cabang baru memaksa kita berkeliling ke seluruh match yang relevan — lengan yang sembunyi di balik wildcard _ tetap diam, jadi pola wildcard perlu dipakai dengan sadar.
Perbandingan Ukuran Memori #
Bandingkan jejak memori kedua pendekatan pada program yang menampung tipe yang sama:
use std::mem::size_of;
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),
}
fn main() {
println!("Circle : {} byte", size_of::<Circle>());
println!("Rect : {} byte", size_of::<Rect>());
println!("ShapeKind (enum) : {} byte", size_of::<ShapeKind>());
println!("Box<dyn Shape> : {} byte", size_of::<Box<dyn Shape>>());
}Output yang Dihasilkan:
Circle : 8 byte
Rect : 16 byte
ShapeKind (enum) : 24 byte
Box<dyn Shape> : 16 byteMari kita bedah kode di atas:
- Baris 28-31 (
enum ShapeKind { Circle(Circle), Rect(Rect) }): enum pembungkus tipe konkrit — varianCirclemenyimpan structCircleutuh di dalamnya, bukan pointer. - Baris 36 (
size_of::<ShapeKind>()): menghasilkan24— tag varian selebar 8 byte (agar sejajar dengan field 8 byte) ditambah varian terbesarRectsebesar 16 byte. Inilah aturan enum: ukurannya selalu cukup untuk varian terbesar, ditambah tag. - Baris 37 (
size_of::<Box<dyn Shape>>()): menghasilkan16— fat pointer di stack. Angka tersebut lebih kecil dari enum, tetapi belum memuat data: nilaiCircleatauRectyang sebenarnya hidup terpisah di heap dengan biaya alokasi per nilai.
Pandangan yang jujur: ShapeKind lebih gemuk di stack (24 byte vs 16 byte) namun tidak menyentuh heap sama sekali — semua data inline, satu alokasi per elemen Vec, tidak ada vtable, tidak ada pointer data yang harus diikuti. Box<dyn Shape> ramping di stack tetapi membayar satu alokasi heap per nilai plus fat pointer plus lompatan vtable saat method dipanggil. Untuk koleksi besar berisi nilai kecil, enum hampir selalu lebih hemat secara total.
Kapan Enum Mengungguli dyn Trait #
Keputusan akhirnya bergantung pada sifat himpunan tipe, bukan pada selera. Enum menang ketika daftar tipe tertutup dan berada di bawah kendali kita: data inline membuat penyalinan dan pemindahan murah, match mengikat field tanpa indirection, exhaustive checking memaksa setiap penambahan varian ditangani tuntas, dan tidak ada biaya dynamic dispatch sedikit pun. Trait object menang ketika daftar tipe terbuka: tipe baru dari crate lain, plugin yang dimuat belakangan, atau komponen yang ditulis pengguna library kita — situasi di mana menambah varian enum setiap ada tipe baru tidak mungkin dilakukan.
Ringkasan #
Enum mencapai polimorfisme lewat varian dan match: himpunan tipe tertutup dikodekan dalam satu tipe berukuran pasti, method memilih cabang saat kompilasi tanpa vtable, dan error E0004 menjamin tidak ada varian baru yang terlupakan. Jejak memorinya data-inline — 24 byte untuk ShapeKind menampung seluruh isi — tanpa satu pun alokasi heap, kontras dengan Box<dyn Shape> yang ramping di stack tetapi membayar alokasi dan indirection di heap. Kekuatan enum justru lahir dari keterbatasannya: menambah varian menuntut pemeriksaan ulang kode. Bab terakhir section ini merangkum seluruh perbandingan — generik, dyn Trait, dan enum — ke dalam satu tabel keputusan.