Enum pada chapter sebelumnya hanya menjawab “yang aktif yang mana”. Padahal pilihan di dunia nyata jarang sesederhana itu: satu peristiwa berarti pilihan sekaligus data yang menyertainya, dan datanya berbeda bentuk untuk tiap pilihan. Pesan chat bisa berupa teks (perlu pengirim dan isi), stiker (perlu kode stiker), transfer file (perlu nama file dan ukuran), atau kabar peserta keluar (tidak perlu apa-apa). Kalau dipaksa ke struct, semua field itu harus eksklusif dan setengahnya kosong; kalau dipaksa ke string, kita kembali ke rawan salah ketik. Rust mengizinkan setiap variant enum membawa data dengan bentuk berbeda-beda — dan pada chapter ini kita akan memakainya untuk memodelkan riwayat chat, lalu membedah bagaimana Rust menyimpan enum semacam itu di memori.

Tiga Bentuk Variant yang Membawa Data #

Sebelum masuk contoh, mari kita kenali tiga bentuk variant yang tersedia. Ketiganya sudah pernah kita lihat di bentuk lain pada chapter-chapter sebelumnya:

  • Tuple variant — data ditulis dalam kurung setelah nama, seperti Sticker(String). Bentuknya identik dengan tuple struct, dan datanya diakses lewat pola.
  • Struct variant — data ditulis sebagai field bernama dalam kurung kurawal, seperti Text { sender: String, content: String }. Bentuknya identik dengan struct bernama, satu variant bisa memuat beberapa field.
  • Unit variant — tanpa data sama sekali, seperti Left. Sama seperti unit struct; variant ini hanya menyatakan “peristiwa ini terjadi”.

Ketiga bentuk tersebut bebas dicampur dalam satu enum. Kombinasi inilah yang membuat enum dengan data sering disebut sebagai tipe “pesan bertipe” — setiap variant adalah jenis pesan yang membawa kargo sesuai kebutuhannya sendiri.

Contoh: Riwayat Pesan Chat #

Program berikut memodelkan empat peristiwa ruang chat lalu mencetaknya satu per satu. Perhatikan bahwa pencetakan setiap jenis pesan membutuhkan data yang berbeda — dan match mengeluarkan data tersebut dari dalam variant-nya:

rust
enum ChatMessage {
    Text { sender: String, content: String },
    Sticker(String),
    FileTransfer { filename: String, size_kb: u64 },
    Left,
}

fn show_message(message: ChatMessage) {
    match message {
        ChatMessage::Text { sender, content } => {
            println!("[{}] {}", sender, content);
        }
        ChatMessage::Sticker(code) => {
            println!("[stiker: {}]", code);
        }
        ChatMessage::FileTransfer { filename, size_kb } => {
            println!("[file: {} ({} KB)]", filename, size_kb);
        }
        ChatMessage::Left => {
            println!("[peserta keluar dari ruangan]");
        }
    }
}

fn main() {
    let history = [
        ChatMessage::Text {
            sender: String::from("Budi"),
            content: String::from("Halo semua, malam!"),
        },
        ChatMessage::Sticker(String::from("jempol_kecil")),
        ChatMessage::FileTransfer {
            filename: String::from("laporan.pdf"),
            size_kb: 812,
        },
        ChatMessage::Left,
    ];

    for message in history {
        show_message(message);
    }

    println!("Ukuran ChatMessage: {} byte", std::mem::size_of::<ChatMessage>());
    println!("Ukuran varian Sticker: {} byte", std::mem::size_of::<String>());
}

Output yang Dihasilkan:

text
[Budi] Halo semua, malam!
[stiker: jempol_kecil]
[file: laporan.pdf (812 KB)]
[peserta keluar dari ruangan]
Ukuran ChatMessage: 48 byte
Ukuran varian Sticker: 24 byte

Mari kita bedah kode di atas baris per baris:

  1. Baris 1-6 (enum ChatMessage { ... }): satu enum memuat empat variant dengan tiga bentuk berbeda — Text adalah struct variant berdua field, Sticker tuple variant berisi satu String, FileTransfer struct variant berisi String dan u64, dan Left unit variant polos.
  2. Baris 8 (fn show_message(message: ChatMessage)): fungsi menerima kepemilikan pesan. Setiap pesan diproses sekali lalu selesai, jadi mengambil ownership memang tepat — tidak ada alasan meminjam data yang akan dibaca sekali.
  3. Baris 10 (ChatMessage::Text { sender, content } => {): pola cabang ini melakukan dua hal sekaligus — mencocokkan bahwa pesan adalah variant Text, dan membongkar (destructuring) field-nya ke variabel sender serta content. Karena fungsi memiliki message, field String di dalamnya ikut dimiliki oleh variabel hasil pembongkaran.
  4. Baris 13 (ChatMessage::Sticker(code) => {): pola tuple variant membongkar isi kurung ke variabel code. Bandingkan bentuknya dengan pembongkaran tuple struct let RgbColor(r, g, b) = ... — pola tuple variant bekerja dengan cara yang sama.
  5. Baris 16 (ChatMessage::FileTransfer { filename, size_kb } => {): struct variant dibongkar dengan menyebut nama field-nya. filename menerima String, size_kb menerima u64.
  6. Baris 19 (ChatMessage::Left => {): unit variant tidak punya data untuk dibongkar, jadi polanya hanya nama variant.
  7. Baris 26-37 (let history = [ ... ]): empat nilai dibuat dengan sintaks yang sesuai bentuk variant-nya — struct variant memakai { field: nilai }, tuple variant memakai (nilai), unit variant cukup ditulis polos. Seluruh elemen array bertipe ChatMessage meskipun kargonya berbeda-beda.
  8. Baris 39-40 (for message in history { ... }): tiap pesan dipindahkan ke show_message satu per satu. Karena pesan berisi String, kepemilikan teks pindah — dan memang sudah tidak dibutuhkan lagi setelah dicetak.

Enum dengan Data di Balik Layar #

Dua baris terakhir output memuat angka yang menjelaskan layout enum di memori. Tipe ChatMessage berukuran 48 byte, padahal variant Left tidak menyimpan apa pun dan Sticker hanya menyimpan satu String (24 byte). Jawabannya: Rust mengalokasikan ruang untuk variant terbesar, lalu seluruh variant berbagi ruang tersebut — ditambah satu angka pembeda (discriminant) yang mencatat variant mana yang sedang aktif. Variant Text dengan dua String adalah yang terbesar (2 × 24 = 48 byte), maka itulah ukuran seluruh tipe. Nilai ChatMessage::Left memakai slot yang sama tetapi hanya membaca bagian pembedanya — sisa ruang dibiarkan tidak terpakai.

Konsekuensi ukuran seragam ini penting untuk dipahami. Array history dapat berisi empat nilai berbentuk berbeda karena setiap elemen menempati slot berukuran sama persis. Bila setiap variant diperbolehkan punya ukuran berbeda, penjumlahan alamat elemen array menjadi tidak terhitung. Rust memilih kesederhanaan akses: ukuran tipe diseragamkan mengikuti variant terbesar, sekecil apa pun variant yang sedang aktif.

Perpaduan Enum dengan Struct #

Enum dengan data jarang berdiri sendiri di program nyata. Pola yang paling lazim: struct memodelkan entitas utama, enum memodelkan keadaan atau peristiwanya, dan struct menyimpan enum sebagai field — misalnya Order { code, buyer, total, status: OrderStatus }. Pola pembacaannya bertingkat: field status menjawab “pesanan ini sedang di tahap mana”, sementara field lain menjawab “pesanan ini milik siapa dan berapa nilainya”. Pemisahan peran tersebut membuat kedua pertanyaan bisa berubah independen — tahap bergerak maju tanpa menyentuh data identitas.

Pilihan bentuk variant pun mengikuti kebutuhan data. Variant dengan satu nilai berurutan baku cocok memakai tuple variant seperti Sticker(String); variant dengan beberapa nilai yang maknanya berbeda lebih jelas memakai struct variant seperti Text { sender, content } karena nama field mengunci makna tiap posisi; peristiwa tanpa kargo cukup unit variant. Kesepakatan bentuk ini membuat seluruh enum terbaca seperti kamus kecil: satu nama variant, satu makna. Kombinasi lengkapnya akan kita rakit tuntas pada chapter penutup Section ini. Sebelum ke sana, masih ada satu pemain penting yang harus kita kenal: enum bawaan Rust yang paling sering dipakai di seluruh bahasa ini — Option, pengganti null.