Setiap bahasa pemrograman akhirnya berhadapan dengan pertanyaan yang sama: bagaimana menyatakan “nilai ini saat ini tidak ada”? Solusi paling umum sepanjang sejarah adalah null — penanda kosong yang bisa masuk ke variabel bertipe apa pun. Penemu konsep ini, Tony Hoare, kelak menyebut penemuannya sebagai kesalahan seharga miliaran dolar, karena akses pada nilai null tidak pernah diperiksa oleh kompilator dan meledak menjadi kegagalan runtime di lingkungan produksi. Rust mengambil jalan berbeda yang radikal: bahasa ini tidak punya null sama sekali. Sebagai gantinya, Rust menyediakan Option<T> — enum bawaan yang menaungi nilai dengan dua variant: Some membawa nilai, None menyatakan ketiadaan. Ketiadaan menjadi bagian dari tipe, sehingga kompilator dapat memaksa kita menangani keduanya. Pada chapter ini kita akan memakai Option untuk pencarian data, mengambil isinya, dan menyimpannya sebagai field struct.

Option Adalah Enum #

Seluruh bekal kita dari dua chapter enum sebelumnya berlaku langsung, karena Option bukan sihir khusus — Option adalah enum biasa milik pustaka standar. Bentuk sederhananya seperti ini:

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

Definisi tersebut memuat satu elemen baru: T adalah type parameter — tempat duduk untuk tipe apa pun yang kelak dipegang variant Some. Option<u32> berarti “mungkin ada u32”, Option<String> berarti “mungkin ada String”. Kita cukup memakai mekanisme ini tanpa harus memahami cara membuat tipe generik sendiri — itu materi Course Generics. Karena Option dipakai sangat sering, kedua variant-nya bisa ditulis langsung tanpa awalan: Some(...) dan None, tanpa Option::.

Membuat dan Memeriksa Option #

Langkah pertama kita sederhana: membuat nilai Option dan memeriksanya. Kedua fungsi pemeriksa yang paling mendasar adalah is_some() (apakah berisi Some?) dan is_none() (apakah None?):

rust
fn main() {
    let jackpot: Option<u64> = Some(5000000);
    let regular: Option<u64> = None;

    println!("Jackpot berisi sesuatu? {}", jackpot.is_some());
    println!("Regular berisi sesuatu? {}", regular.is_some());
    println!("Jackpot kosong? {}", jackpot.is_none());
}

Output yang Dihasilkan:

text
Jackpot berisi sesuatu? true
Regular berisi sesuatu? false
Jackpot kosong? false

Penjelasan singkatnya:

  1. Baris 2 (let jackpot: Option<u64> = Some(5000000);): variant Some dibuat dengan membungkus nilainya dalam kurung — Some(5000000) berarti “ada nilainya, yaitu 5000000”. Anotasi tipe Option<u64> memberi tahu kompilator tipe apa yang dibungkus.
  2. Baris 3 (let regular: Option<u64> = None;): variant None berdiri sendiri tanpa isi. None bukan angka 0 dan bukan string kosong — None adalah ketiadaan yang sepenuhnya terpisah dari nilai apa pun.
  3. Baris 5-7 (is_some() dan is_none()): kedua method mengembalikan bool. jackpot memegang Some, jadi is_some() bernilai true; regular memegang None, jadi is_some() bernilai false.

Perhatikan konsekuensi penting dari sistem tipe ini: variabel bertipe Option<u64> tidak bisa langsung dijumlahkan dengan angka. Angka 5000000 tersembunyi di dalam Some dan harus diambil lebih dulu — kompilator menolak campur tangan angka pada nilai yang mungkin kosong.

Fungsi yang Mengembalikan Option #

Kebiasaan lama dari dunia null adalah mengembalikan nilai khusus sebagai tanda gagal: -1 untuk pencarian yang tidak ketemu, atau null pointer untuk data yang kosong. Kedua konvensi itu menyerahkan pengecekan pada kebajikan pembaca kode. Pola Rust menyatakan kemungkinan gagal langsung pada tipe nilai kembalian. Mari kita tulis pencarian nilai ujian pertama yang melampaui ambang batas:

rust
fn find_first_above(scores: &[u32], threshold: u32) -> Option<u32> {
    for &score in scores {
        if score > threshold {
            return Some(score);
        }
    }
    None
}

fn main() {
    let scores = [78, 85, 60, 92, 71];

    let strong = find_first_above(&scores, 80);
    let perfect = find_first_above(&scores, 95);

    match strong {
        Some(value) => println!("Nilai pertama di atas 80: {}", value),
        None => println!("Tidak ada nilai di atas 80"),
    }

    match perfect {
        Some(value) => println!("Nilai pertama di atas 95: {}", value),
        None => println!("Tidak ada nilai di atas 95"),
    }

    if strong.is_some() {
        println!("Data kuat ditemukan");
    }

    let easy = find_first_above(&scores, 50);
    println!("Nilai pertama di atas 50: {}", easy.unwrap());
}

Output yang Dihasilkan:

text
Nilai pertama di atas 80: 85
Tidak ada nilai di atas 95
Data kuat ditemukan
Nilai pertama di atas 50: 78

Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (fn find_first_above(scores: &[u32], threshold: u32) -> Option<u32>): tipe nilai kembalian Option<u32> menjanjikan dua kemungkinan sejak tanda tangan — Some(u32) bila ketemu, None bila tidak. Pemanggil tidak bisa mengabaikan janji ini.
  2. Baris 2 (for &score in scores): parameter bertipe slice &[u32] dipinjam dari pemanggil; pola &score menyalin setiap elemen u32 (tipe Copy) ke variabel score satu per satu.
  3. Baris 4 (return Some(score);): begitu satu nilai melampaui ambang, fungsi langsung pulang membawa Some(score) — nilai 85 pada pencarian ambang 80.
  4. Baris 7 (None): kalau loop selesai tanpa temuan, baris terakhir fungsi mengembalikan None sebagai nilai ekspresi. Dua titik keluar inilah wajah dari “mungkin tidak ada”.
  5. Baris 16-19 (match strong { ... }): pencocokan penuh atas Option — cabang Some(value) membongkar isi dan mengikatnya ke value, cabang None menangani ketiadaan. Karena strong berisi Some(85), cabang pertama yang berjalan.
  6. Baris 26-28 (if strong.is_some()): untuk keputusan sederhana ya/tidak, is_some() menghindari upacara match lengkap.
  7. Baris 30-31 (easy.unwrap()): method unwrap() mengeluarkan isi Some secara langsung. Pencarian ambang 50 berhenti di 78, jadi unwrap() mengembalikan 78 dengan aman.

unwrap: Jalan Pintas yang Bersyarat #

unwrap() menyingkat satu langkah: bila nilainya Some, isi keluar; bila None, program panic. Kedua wajahnya harus kita lihat sendiri. Berikut pemanggilan pencarian yang pasti gagal — tidak ada nilai di atas 95:

rust
fn find_first_above(scores: &[u32], threshold: u32) -> Option<u32> {
    for &score in scores {
        if score > threshold {
            return Some(score);
        }
    }
    None
}

fn main() {
    let scores = [78, 85, 60, 92, 71];

    let perfect = find_first_above(&scores, 95);

    println!("Nilai pertama di atas 95: {}", perfect.unwrap());
}

Pesan panic saat program berjalan:

text
thread 'main' (16363) panicked at main.rs:15:54:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Pesan panic tersebut adalah perilaku runtime yang nyata. Panic menyetop program dengan pesan kesalahan called Option::unwrap()on aNone value lengkap dengan lokasi baris dan kolom pemanggilan — angka dalam kurung setelah main adalah penanda thread yang nilainya berubah pada setiap eksekusi. Perhatikan perbedaan mendasarnya dengan dunia null: kegagalan unwrap pada None terjadi karena kita secara eksplisit memilih shortcut, bukan karena lubang tersembunyi di bahasa. Nilai Option yang belum diambil isinya tidak akan pernah meledak diam-diam.

Option sebagai Field Struct #

Pola pemakaian Option yang paling sering dijumpai: field struct yang memang tidak selalu terisi. Siswa boleh punya panggilan akrab, boleh juga tidak — dan model datanya menjadi jujur lewat Option<String>:

rust
struct Student {
    name: String,
    nickname: Option<String>,
    score: u32,
}

fn show_student(student: &Student) {
    match &student.nickname {
        Some(nick) => println!("{} (biasa dipanggil {}): {}", student.name, nick, student.score),
        None => println!("{}: {}", student.name, student.score),
    }
}

fn main() {
    let budi = Student {
        name: String::from("Budi Santoso"),
        nickname: Some(String::from("Bud")),
        score: 85,
    };

    let rina = Student {
        name: String::from("Rina Wijaya"),
        nickname: None,
        score: 92,
    };

    show_student(&budi);
    show_student(&rina);
}

Output yang Dihasilkan:

text
Budi Santoso (biasa dipanggil Bud): 85
Rina Wijaya: 92

Penjelasan baris-baris pentingnya:

  1. Baris 3 (nickname: Option<String>): field ini menyatakan dengan jujur bahwa panggilan akrab itu opsional. Pemakai tipe Student dipaksa menghadapi dua kemungkinan setiap kali membaca field ini — kompilator menjaganya.
  2. Baris 8-11 (match &student.nickname): karena fungsi hanya meminjam struct, pola match bekerja lewat reference. Cabang Some(nick) meminjam isi String ke variabel nick; cabang None mencetak nama tanpa julukan. Kedua siswa mendapat perlakuan yang benar — Budi dengan julukan, Rina tanpa.
  3. Baris 15-25 (instansiasi budi dan rina): satu field yang sama diisi Some(String::from("Bud")) pada siswa pertama dan None pada siswa kedua. Tidak ada string kosong palsu, tidak ada null tersembunyi — hanya dua variant yang jelas.

Ringkasan #

Option<T> menggantikan null dengan enum: Some membawa nilai, None menyatakan ketiadaan, dan keduanya adalah bagian dari tipe sehingga kompilator memaksa penanganan. Fungsi pencarian mengembalikan Option alih-alih nilai sihir seperti -1; match membongkar Some sambil menuntut cabang None; is_some() dan is_none() menjawab pertanyaan ya/tidak; dan unwrap() mengambil isi dengan risiko panic pada None. Field struct opsional seperti julukan siswa dimodelkan apa adanya lewat Option<String>. Kemampuan tipe mencetak dirinya sendiri dan membandingkan dirinya sendiri masih harus kita tulis manual — atau lebih tepatnya, dideklarasikan otomatis. Cara singkatnya bernama derive, chapter berikutnya.