Banyak data di dunia nyata bukan berupa kumpulan atribut, melainkan satu pilihan dari beberapa kemungkinan: lampu lalu lintas sedang merah, kuning, atau hijau — tepat satu dari tiga, tidak pernah dua sekaligus. Status pesanan toko daring berada di salah satu tahap: menunggu, dikemas, dikirim, diterima, atau dibatalkan. Kebiasaan lama dari banyak bahasa adalah mewakili pilihan ini dengan angka (status = 3) atau teks (status = "shipped") — pendekatan yang rawan salah ketik, sulit dilacak, dan tidak diperiksa kelengkapannya. Rust menyediakan enum: tipe yang nilainya terbatas pada daftar variant bernama yang kita definisikan sendiri. Pada chapter ini kita akan mendefinisikan enum, mencocokkannya dengan match, dan memahami representasinya di memori.

Masalah Representasi Pilihan #

Coba bandingkan dua penulisan status pesanan. Versi pertama memakai teks bebas, versi kedua memakai angka khusus — keduanya dikompilasi tanpa keluhan:

rust
fn main() {
    let status = String::from("dikirm"); // salah ketik: seharusnya "dikirim"
    let code: u8 = 7; // 7 itu apa? harus membaca dokumen terpisah

    println!("Status teks: {}", status);
    println!("Status angka: {}", code);
}

Output yang Dihasilkan:

text
Status teks: dikirm
Status angka: 7

Salah ketik "dikirm" melaju tanpa peringatan, dan angka 7 tidak bercerita apa pun kepada pembaca kode. Kesalahan baru muncul saat program berjalan — pesanan hilang, diskon salah terpasang, atau laporan kosong. Enum memindahkan pemeriksaan ini ke masa kompilasi: daftar kemungkinan ditutup (kita tak bisa menulis variant yang tidak terdaftar), setiap kemungkinan punya nama yang terbaca, dan match menolak bekerja bila ada kemungkinan yang belum tertangani.

Mendefinisikan Enum dan Mencocokkannya #

Definisi enum dimulai dengan kata kunci enum, diikuti nama tipe dan daftar variant di dalam blok. Mari kita mulai dari contoh paling ringkas — lampu lalu lintas:

rust
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let lights = [TrafficLight::Red, TrafficLight::Yellow, TrafficLight::Green];

    for light in lights {
        let action = match light {
            TrafficLight::Red => String::from("berhenti"),
            TrafficLight::Yellow => String::from("siap-siap"),
            TrafficLight::Green => String::from("melaju"),
        };

        println!("Aksi pengemudi: {}", action);
    }
}

Output yang Dihasilkan:

text
Aksi pengemudi: berhenti
Aksi pengemudi: siap-siap
Aksi pengemudi: melaju

Mari kita bedah kode di atas baris per baris:

  1. Baris 1-5 (enum TrafficLight { ... }): kata kunci enum memperkenalkan tipe baru. Tiga variant dideklarasikan — Red, Yellow, Green. Pada chapter ini seluruh variant belum membawa data; mereka hanyalah nama-nama nilai.
  2. Baris 8 (let lights = [TrafficLight::Red, ...]): nilai enum dibuat dengan pola NamaTipe::NamaVariant. Variant tidak berdiri sendiri — Red selalu ditulis sebagai TrafficLight::Red supaya dua enum yang kebetulan memiliki variant senama tetap tidak bertabrakan.
  3. Baris 10-15 (for light in lights): setiap elemen array diiterasi. Perhatikan bahwa semua nilai bertipe sama — TrafficLight — meskipun variant-nya berbeda; pemilihan variant adalah urusan nilai, bukan tipe.
  4. Baris 11-15 (match light { ... }): match mencocokkan nilai dengan daftar pola variant. Ketiga cabang mengembalikan String sebagai hasil ekspresi, lalu hasilnya diikat ke action. Karena TrafficLight tidak mengimplementasikan Copy, pencocokan di baris 11 memindahkan nilai light ke dalam match — aman, karena light memang tidak dipakai lagi setelahnya.
  5. Baris 17 (println!("Aksi pengemudi: {}", action);): hasil pencocokan dicetak. Tiga baris output muncul karena array berisi tiga lampu berbeda.

Contoh yang Lebih Realistis: Status Pesanan #

Toko daring jarang berhenti pada tiga kemungkinan. Berikut model lengkap status pesanan beserta fungsinya — advance menggerakkan status ke tahap berikutnya, describe menerjemahkan status menjadi teks untuk pembeli:

rust
enum OrderStatus {
    Pending,
    Packed,
    Shipped,
    Delivered,
    Cancelled,
}

fn advance(status: OrderStatus) -> OrderStatus {
    match status {
        OrderStatus::Pending => OrderStatus::Packed,
        OrderStatus::Packed => OrderStatus::Shipped,
        OrderStatus::Shipped => OrderStatus::Delivered,
        OrderStatus::Delivered => OrderStatus::Delivered,
        OrderStatus::Cancelled => OrderStatus::Cancelled,
    }
}

fn describe(status: &OrderStatus) -> String {
    match status {
        OrderStatus::Pending => String::from("menunggu pembayaran"),
        OrderStatus::Packed => String::from("sedang dikemas"),
        OrderStatus::Shipped => String::from("sedang dikirim"),
        OrderStatus::Delivered => String::from("sudah diterima"),
        OrderStatus::Cancelled => String::from("dibatalkan"),
    }
}

fn main() {
    let mut status = OrderStatus::Pending;

    println!("Awal     : {}", describe(&status));

    status = advance(status);
    println!("Langkah 1: {}", describe(&status));

    status = advance(status);
    println!("Langkah 2: {}", describe(&status));

    status = advance(status);
    println!("Langkah 3: {}", describe(&status));

    let other_order = OrderStatus::Cancelled;
    println!("Pesanan lain: {}", describe(&other_order));

    println!("Ukuran OrderStatus: {} byte", std::mem::size_of::<OrderStatus>());
}

Output yang Dihasilkan:

text
Awal     : menunggu pembayaran
Langkah 1: sedang dikemas
Langkah 2: sedang dikirim
Langkah 3: sudah diterima
Pesanan lain: dibatalkan
Ukuran OrderStatus: 1 byte

Penjelasan baris-baris pentingnya:

  1. Baris 9-17 (fn advance(...)): fungsi menerima status miliknya (by value) dan mengembalikan status baru. Setiap cabang match memetakan satu variant ke variant berikutnya; Delivered dan Cancelled memetakan ke dirinya sendiri karena kedua status itu bersifat akhir — pesanan yang sudah diterima atau dibatalkan tidak berpindah tahap lagi.
  2. Baris 19-27 (fn describe(status: &OrderStatus)): fungsi ini hanya membaca status, jadi cukup meminjam lewat reference. Cabang-cabang match tetap ditulis dalam bentuk OrderStatus::Pending — Rust secara otomatis menyesuaikan pencocokan pola terhadap reference tanpa perubahan sintaks.
  3. Baris 34 (status = advance(status);): nilai status dipindahkan ke fungsi, diolah menjadi status baru, lalu hasilnya mengisi kembali variabel status. Pola x = f(x) semacam ini sah karena penugasan terjadi setelah pemindahan selesai.
  4. Baris 43 (let other_order = OrderStatus::Cancelled;): pembuatan nilai kedua membuktikan enum menyimpan tepat satu variant per nilai — other_order adalah Cancelled dan hanya Cancelled.
  5. Baris 46 (std::mem::size_of::<OrderStatus>()): kelima variant tanpa data itu dihimpun dalam 1 byte — cukup satu angka pembeda (discriminant) untuk mencatat “nilai ini variant yang mana”.

Enum di Balik Layar: Satu Byte untuk Semua Pilihan #

Baris terakhir output di atas menyimpan pelajaran layout. Enum tanpa data hanya perlu menyimpan identitas variant yang sedang aktif, sehingga seluruh tipe OrderStatus berukuran 1 byte — bandingkan dengan struct yang harus menyimpan seluruh field sekaligus. Prinsip ini menegaskan sifat paling penting enum: satu nilai enum berada di tepat satu variant pada satu waktu, bukan gabungan. Lampu tidak bisa merah sekaligus hijau; pesanan tidak bisa dikirim sekaligus dibatalkan. Justru pembatasan inilah yang membuat match dapat memeriksa kelengkapan penanganan secara otomatis.

Angka 1 byte tersebut berasal dari satu bilangan pembeda internal bernama discriminant — nilai tersembunyi yang mencatat “nilai ini sedang menjadi variant yang mana”. Kita tidak pernah membaca atau menulis discriminant secara langsung; kompilator yang mengisinya saat kita membuat OrderStatus::Shipped dan membacanya saat match mencocokkan pola. Variant bertambah tidak serta-merta menaikkan ukuran tipe, karena 1 byte cukup membedakan hingga 256 kemungkinan.

Kelengkapan tersebut terasa nyata ketika kita sengaja menghapus satu cabang. Berikut describe yang lupa menangani Cancelled:

rust
enum OrderStatus {
    Pending,
    Packed,
    Shipped,
    Delivered,
    Cancelled,
}

fn describe(status: &OrderStatus) -> String {
    match status {
        OrderStatus::Pending => String::from("menunggu pembayaran"),
        OrderStatus::Packed => String::from("sedang dikemas"),
        OrderStatus::Shipped => String::from("sedang dikirim"),
        OrderStatus::Delivered => String::from("sudah diterima"),
    }
}

fn main() {
    let status = OrderStatus::Shipped;
    println!("{}", describe(&status));
}

Pesan error kompilator:

text
error[E0004]: non-exhaustive patterns: `&OrderStatus::Cancelled` not covered
  --> main.rs:10:11
   |
10 |     match status {
   |           ^^^^^^ pattern `&OrderStatus::Cancelled` not covered
   |
note: `OrderStatus` defined here
  --> main.rs:1:6
   |
 1 | enum OrderStatus {
   |      ^^^^^^^^^^^
...
 6 |     Cancelled,
   |     --------- not covered
   = note: the matched value is of type `&OrderStatus`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
14 ~         OrderStatus::Delivered => String::from("sudah diterima"),
15 ~         &OrderStatus::Cancelled => todo!(),
   |

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0004`.

Pesan E0004 menyebutkan dengan gamblang variant mana yang belum tertutup dan bahkan menawarkan kerangka cabang perbaikan. Bandingkan dengan pendekatan teks "shipped": satu status yang lupa ditangani tidak menghasilkan error apa pun, hanya panic atau cabang kosong saat runtime.

Ringkasan #

Enum merepresentasikan satu pilihan dari daftar kemungkinan bernama: variant dibuat dengan Type::Variant, nilai selalu berada tepat satu variant, dan match menuntut seluruh variant tertangani (E0004 bila ada yang terlewat). Lima variant tanpa data cukup dihimpun dalam 1 byte karena enum hanya menyimpan identitas variant aktif. Seluruh contoh kita sejauh ini memakai variant polos — padahal Rust memungkinkan setiap variant membawa data yang berbeda bentuk, dan kemampuan itulah yang akan kita buka pada Chapter Enum dengan Data.