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:
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?):
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:
Jackpot berisi sesuatu? true
Regular berisi sesuatu? false
Jackpot kosong? falsePenjelasan singkatnya:
- Baris 2 (
let jackpot: Option<u64> = Some(5000000);): variantSomedibuat dengan membungkus nilainya dalam kurung —Some(5000000)berarti “ada nilainya, yaitu 5000000”. Anotasi tipeOption<u64>memberi tahu kompilator tipe apa yang dibungkus. - Baris 3 (
let regular: Option<u64> = None;): variantNoneberdiri sendiri tanpa isi.Nonebukan angka0dan bukan string kosong —Noneadalah ketiadaan yang sepenuhnya terpisah dari nilai apa pun. - Baris 5-7 (
is_some()danis_none()): kedua method mengembalikanbool.jackpotmemegangSome, jadiis_some()bernilaitrue;regularmemegangNone, jadiis_some()bernilaifalse.
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:
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:
Nilai pertama di atas 80: 85
Tidak ada nilai di atas 95
Data kuat ditemukan
Nilai pertama di atas 50: 78Mari kita bedah kode di atas baris per baris:
- Baris 1 (
fn find_first_above(scores: &[u32], threshold: u32) -> Option<u32>): tipe nilai kembalianOption<u32>menjanjikan dua kemungkinan sejak tanda tangan —Some(u32)bila ketemu,Nonebila tidak. Pemanggil tidak bisa mengabaikan janji ini. - Baris 2 (
for &score in scores): parameter bertipe slice&[u32]dipinjam dari pemanggil; pola&scoremenyalin setiap elemenu32(tipeCopy) ke variabelscoresatu per satu. - Baris 4 (
return Some(score);): begitu satu nilai melampaui ambang, fungsi langsung pulang membawaSome(score)— nilai85pada pencarian ambang80. - Baris 7 (
None): kalau loop selesai tanpa temuan, baris terakhir fungsi mengembalikanNonesebagai nilai ekspresi. Dua titik keluar inilah wajah dari “mungkin tidak ada”. - Baris 16-19 (
match strong { ... }): pencocokan penuh atasOption— cabangSome(value)membongkar isi dan mengikatnya kevalue, cabangNonemenangani ketiadaan. KarenastrongberisiSome(85), cabang pertama yang berjalan. - Baris 26-28 (
if strong.is_some()): untuk keputusan sederhana ya/tidak,is_some()menghindari upacaramatchlengkap. - Baris 30-31 (
easy.unwrap()): methodunwrap()mengeluarkan isiSomesecara langsung. Pencarian ambang50berhenti di78, jadiunwrap()mengembalikan78dengan 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:
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:
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 backtracePesan 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>:
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:
Budi Santoso (biasa dipanggil Bud): 85
Rina Wijaya: 92Penjelasan baris-baris pentingnya:
- Baris 3 (
nickname: Option<String>): field ini menyatakan dengan jujur bahwa panggilan akrab itu opsional. Pemakai tipeStudentdipaksa menghadapi dua kemungkinan setiap kali membaca field ini — kompilator menjaganya. - Baris 8-11 (
match &student.nickname): karena fungsi hanya meminjam struct, polamatchbekerja lewat reference. CabangSome(nick)meminjam isiStringke variabelnick; cabangNonemencetak nama tanpa julukan. Kedua siswa mendapat perlakuan yang benar —Budidengan julukan,Rinatanpa. - Baris 15-25 (instansiasi
budidanrina): satu field yang sama diisiSome(String::from("Bud"))pada siswa pertama danNonepada siswa kedua. Tidak ada string kosong palsu, tidak adanulltersembunyi — 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.