Enum dengan data sudah kita kuasai: setiap variant boleh membawa muatan bertipe berbeda, dan match membongkarnya dengan pola. Kini kita tumpangi kemampuan itu dengan type parameter, dan enum menjadi wadah yang muatannya berubah-ubah sesuai pemakai. Lebih menarik lagi, babak ini mengandung pengungkapan: Option dan Result yang kita pakai sejak Section Struktur Data ternyata adalah enum generik buatan standard library. Setelah menyadari hal itu, sintaks Option<String> yang selama ini kita tulis sebagai anotasi tipe akan terbaca sebagai instansiasi — persis seperti Locker<String> pada chapter sebelumnya.

Enum Milik Sendiri dengan Muatan Fleksibel #

Kita bangun pelacak status pengiriman paket. Sebuah paket bisa sedang mengantre, dibawa kendaraan tertentu, atau sudah tiba di alamat tujuan — dan alamat itu bisa berupa String, bisa pula tipe identitas lain tergantung siapa yang memakai enum ini:

rust
enum Delivery<T> {
    Queued,
    OnVehicle(u8),
    Delivered(T),
}

fn main() {
    let statuses = [
        Delivery::Queued,
        Delivery::OnVehicle(3),
        Delivery::Delivered(String::from("Jalan Kenanga No. 12")),
    ];

    for status in &statuses {
        let label = match status {
            Delivery::Queued => String::from("masih menunggu kurir"),
            Delivery::OnVehicle(vehicle) => format!("dikendaraan oleh kurir nomor {}", vehicle),
            Delivery::Delivered(address) => format!("sampai ke {}", address),
        };
        println!("Status: {}", label);
    }
}

Output yang Dihasilkan:

text
Status: masih menunggu kurir
Status: dikendaraan oleh kurir nomor 3
Status: sampai ke Jalan Kenanga No. 12

Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (enum Delivery<T> {): type parameter T dideklarasikan setelah nama enum dan berlaku untuk seluruh body. Enum generik mengikuti aturan yang sama dengan struct generik dari chapter sebelumnya.
  2. Baris 2 (Queued,): variant tanpa data — variant ini sama sekali tidak memakai T, dan itu diperbolehkan. Tidak semua variant wajib menyentuh type parameter.
  3. Baris 3 (OnVehicle(u8),): variant berdata dengan tipe tetap. Nomor kendaraan memang selalu u8 untuk semua pemakai enum ini.
  4. Baris 4 (Delivered(T),): variant berdata dengan muatan generik. Pemakai yang butuh alamat String mendapat Delivery<String>; pemakai lain bisa memakai Delivery<u32> untuk kode area.
  5. Baris 8-12 (array statuses): tiga nilai disimpan dalam satu array. Variant Queued dan OnVehicle(3) tidak memberi petunjuk tentang T, tetapi elemen Delivered(String::from(...)) menegaskan T = String, dan inferensi menyatukan seluruh array menjadi [Delivery<String>; 3].
  6. Baris 15-19 (match status): pencocokan pola berjalan persis seperti enum dengan data biasa — variant dicocokkan namanya, muatan diikat ke variabel vehicle dan address. Pada instansiasi ini address bertipe String (via reference &String karena iterasi meminjam array).

Option dan Result Telah Lama Menjadi Enum Generik #

Sekarang momen pengungkapannya. Definisi Option dan Result di standard library — versi yang disederhanakan — tampak seperti ini:

rust
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Dua enum ini persis pola Delivery<T> yang baru kita tulis. Option mendeklarasikan satu type parameter: Some(T) membawa nilai keberhasilan-ada, None tidak membawa apa-apa. Result mendeklarasikan dua: Ok(T) membawa nilai sukses, Err(E) membawa data error — perhatikan penamaan T dan E yang deskriptif untuk success type dan error type. Prelude Rust — daftar nama yang diimpor otomatis ke setiap file — memuat keduanya, sehingga Option<String> dan Result<u8, String> langsung tersedia tanpa use apa pun. (Kedua definisi di atas adalah ilustrasi bentuk aslinya; kita tidak perlu dan tidak boleh mendefinisikannya ulang karena prelude sudah menyediakannya.)

Seluruh fitur yang selama ini kita nikmati dari kedua enum itu kini punya penjelasan struktural. Anotasi Option<u32> adalah pemilihan instansiasi, sama seperti Locker<String>. Pencocokan match Some(value) bekerja karena variant-nya memang Some(T). Dan aturan bahwa Result<T, E> membawa dua tipe yang berbeda peran — nilai sukses dan data error — terbaca langsung dari dua type parameternya.

Wujud pemakaiannya dalam satu program:

rust
fn main() {
    let stock: Option<u32> = Some(12);
    let memo: Option<String> = Some(String::from("Sisa 3 dus di gudang B"));
    let promo: Result<u8, String> = Err(String::from("kode promo kadaluarsa"));

    println!("Stok: {:?}", stock);
    println!("Catatan: {:?}", memo);
    println!("Promo: {:?}", promo);
}

Output yang Dihasilkan:

text
Stok: Some(12)
Catatan: Some("Sisa 3 dus di gudang B")
Promo: Err("kode promo kadaluarsa")

Mari kita bedah kode di atas baris per baris:

  1. Baris 2 (let stock: Option<u32> = Some(12);): anotasi Option<u32> memilih instansiasi; Some(12) membawa muatan u32. Standard library mengimplementasikan Debug untuk Option<T> selama T juga Debug, sehingga {:?} sah mencetaknya.
  2. Baris 3 (let memo: Option<String> = ...): instansiasi kedua dengan muatan String — Option<String> dan Option<u32> adalah dua tipe berbeda meski berasal dari satu definisi enum.
  3. Baris 4 (let promo: Result<u8, String> = Err(...)): anotasi pada Result ini bukan hiasan — Err(String::from(...)) hanya menegaskan E = String dan sama sekali tidak memberi petunjuk tentang T, sehingga tanpa anotasi penuh kompilator akan kehabisan informasi. Nilai Err menyimpan data error; T tetangga suksesnya belum terpakai di nilai ini.
  4. Baris 6-8 ({:?}): formatter {:?} mencetak enum secara utuh beserta variant dan muatannya, dan keluarannya memperlihatkan struktur internal Some(...) serta Err(...) secara gamblang.

Kapan Enum Generik Layak Dipakai #

Enum generik bersinar pada dua pola. Pertama, mesin status yang muatannya bergantung pemakai: Delivery<T> tadi, atau widget antarmuka dengan status Loading/Loaded(T) — tipe T dipilih oleh pemanggil, bukan perancang enum. Kedua, tipe wadah serbaguna yang perilaku utamanya mengelilingi muatan: Option dan Result adalah contoh terbesarnya — keduanya tidak peduli isinya apa; keduanya hanya mengelola satu pertanyaan (ada atau tidak, sukses atau gagal). Daftar std akan terus bertambah dengan pola serupa saat kita memasuki tipe pintar pada section berikutnya dari Course ini.

Kedua pola itu berbagi satu ciri: perilaku enum tidak bergantung pada isi muatan. Option memperlakukan Some(12) dan Some("teks") dengan aturan yang sama karena match-nya hanya memisahkan dua variant, bukan mengoperasikan muatannya. Begitu sebuah enum perlu mengoperasikan muatannya — membandingkan, menjumlahkan, mencetak — saat itulah type parameter-nya membutuhkan trait bound, dan babak itu sudah kita kenal dari Chapter Trait Bound.

Ringkasan #

Type parameter <T> pada enum memungkinkan variant membawa muatan yang tipenya diputuskan per pemakai, sementara variant lain boleh memakai tipe tetap atau tanpa data. Dengan lensa ini, Option<T> dan Result<T, E> terbuka isinya: enum generik bawaan std yang instansiasinya — Option<String>, Result<u8, String> — adalah tipe-tipe terpisah yang selama ini kita pakai tanpa menyadarinya. Anotasi tipe eksplisit menjadi penting justru saat data belum memberi petunjuk inferensi, misalnya pada nilai Err yang menyembunyikan T. Enum generik masih menuntut satu penjembatan menuju perilaku: blok impl — dan berbeda dari struct, blok tersebut harus mendeklarasikan ulang parameternya, pokok bahasan Chapter Method Generik.