Rust gak punya null dan gak punya exception. Sebagai gantinya ada dua enum bawaan: Option<T> untuk nilai yang mungkin gak ada, dan Result<T, E> untuk pekerjaan yang bisa gagal, lengkap dengan alasannya. Bayangin kasir Kedai Martabak Bang Rusdi. Pembeli menanyakan harga kopi. Kopi memang gak ada di menu: gak ada yang salah, jawabannya cuma "gak ada". Lain cerita dengan formulir pesanan yang kotak porsinya diisi "dua" pakai huruf. Kasir gak bisa membacanya sebagai angka, dan ia harus bilang kenapa: "porsinya bukan angka". Jawaban "ada, harganya 5000" atau "gak ada" itu Option: Some(5000) atau None. Jawaban "berhasil, 2 porsi" atau "gagal, karena ..." itu Result: Ok(2) atau Err(alasan).
🧪 Tebakan cepat:
harga("es teh").unwrap()dicetak, lalulet kopi = harga("kopi").unwrap();danprintln!("Kopi: {}", kopi);. Apa yang terjadi? Tebak dulu, jawabannya ada di bagian "unwrap dan expect".
Option: mungkin ada, mungkin gak
Ini harga dari pelajaran enum, sekarang dengan es jeruk:
fn harga(menu: &str) -> Option<u32> {
match menu {
"martabak manis" => Some(32000),
"martabak telur" => Some(38000),
"es teh" | "es jeruk" => Some(5000),
_ => None,
}
}
fn main() {
let es = harga("es teh");
let kopi = harga("kopi");
println!("{:?} {:?}", es, kopi);
match kopi {
Some(h) => println!("Kopi {}", h),
None => println!("Kopi gak ada di menu"),
}
if let Some(h) = es {
println!("Es teh {}", h);
}
println!("{}", kopi.unwrap_or(0));
println!("{:?}", es.map(|h| h * 3));
println!("{} {}", es.is_some(), kopi.is_none());
}
Some(5000) None
Kopi gak ada di menu
Es teh 5000
0
Some(15000)
true true
matchharus menanganiSomedanNone. Compiler gak akan membiarkanmu lupa kasus "gak ada".if let Some(h) = es { ... }adalahmatchuntuk satu kasus saja. KalauesituNone, blok itu dilewati diam-diam.unwrap_or(0)memberi isinya, atau 0 kalauNone.map(|h| h * 3)mengubah isi di dalamSome(5000 jadi 15000) dan membiarkanNonetetapNone.Option<u32>bukanu32.harga("es teh") == 5000ditolak denganexpected `Option<u32>`, found integer, dan saran compiler-nyaSome(5000).
Result: berhasil, atau gagal dengan alasan
parse membaca teks jadi angka, dan bisa gagal:
fn main() {
let isian = ["2", "dua", "", "5000000000"];
for teks in isian {
let porsi: Result<u32, _> = teks.parse::<u32>();
println!("{:?}", porsi);
match porsi {
Ok(n) => println!("{} porsi", n),
Err(e) => println!("Gagal: {}", e),
}
}
}
Ok(2)
2 porsi
Err(ParseIntError { kind: InvalidDigit })
Gagal: invalid digit found in string
Err(ParseIntError { kind: Empty })
Gagal: cannot parse integer from empty string
Err(ParseIntError { kind: PosOverflow })
Gagal: number too large to fit in target type
teks.parse::<u32>()memberiResult<u32, ParseIntError>:Ok(angka)atauErr(error)._diResult<u32, _>artinya "isi sendiri tipenya", dan compiler mengisinyaParseIntError.- Error-nya membawa alasan.
{:?}menampilkannya untuk programmer,ParseIntError { kind: InvalidDigit }.{}menampilkannya untuk manusia,invalid digit found in string. "5000000000"gagal karenau32paling besar 4.294.967.295.parseharus tahu tipe tujuannya.let porsi = "2".parse().unwrap();ditolak dengan E0284type annotations needed. Tulisparse::<u32>()ataulet porsi: u32.
Option atau Result?
Pakai Option kalau "gak ada" itu hal biasa yang gak perlu dijelaskan: mencari harga di menu, pesanan.get(3), HashMap::get. Pakai Result kalau pekerjaannya bisa gagal dan pemanggilnya berhak tahu kenapa: membaca angka dari teks, membuka file, memanggil server. Keduanya bisa saling diubah: .ok_or(alasan) mengubah Option jadi Result dengan menambah alasan, dan .ok() mengubah Result jadi Option dengan membuang alasannya.
Fungsi yang memakai keduanya biasanya mengembalikan Result, karena kegagalannya perlu dijelaskan:
fn total(menu: &str, porsi: &str) -> Result<u32, String> {
let h = match harga(menu) {
Some(h) => h,
None => return Err(format!("{} gak ada di menu", menu)),
};
match porsi.parse::<u32>() {
Ok(p) => Ok(h * p),
Err(e) => Err(format!("porsi \"{}\": {}", porsi, e)),
}
}
total("martabak telur", "2") memberi Ok(76000), total("kopi", "2") memberi Err("kopi gak ada di menu"), dan total("es teh", "dua") memberi Err("porsi \"dua\": invalid digit found in string"). Pola "kalau gagal, langsung kembalikan error-nya ke pemanggil" ini sangat sering, jadi Rust punya singkatannya, operator ?. Itu bahan pelajaran berikutnya.
Mengolah tanpa match
fn main() {
let menu = ["martabak manis", "martabak telur", "es teh", "kopi"];
println!("{:?}", menu.get(2).and_then(|m| harga(m)));
println!("{:?}", menu.get(3).and_then(|m| harga(m)));
println!("{:?}", menu.get(9).and_then(|m| harga(m)));
println!("{:?}", harga("kopi").ok_or("kopi gak ada di menu"));
println!("{:?}", harga("es teh").ok_or("es teh gak ada di menu"));
let porsi = "dua".parse::<u32>().unwrap_or(1);
println!("{}", porsi);
println!("{:?}", "2".parse::<u32>().ok());
}
Some(5000)
None
None
Err("kopi gak ada di menu")
Ok(5000)
1
Some(2)
and_thenmenyambung dua langkah yang masing-masing bisaNone: nomor menu ke nama (menu.get(2)), lalu nama ke harga (harga(m)). Nomor 3 ada, tapi kopi gak punya harga; nomor 9 gak ada sama sekali. Dua-duanyaNone. Bedanya denganmap: closuremapmemberi nilai biasa, closureand_thenmemberiOptionlagi, jadi hasilnya gak jadiSome(Some(...)).ok_or("...")menempelkan alasan:NonejadiErr("kopi gak ada di menu"),Some(5000)jadiOk(5000).unwrap_or(1)diResultmemberi 1 kalau gagal. Hati-hati: "dua" diam-diam jadi 1 porsi, padahal pembelinya mau dua. Default hanya aman kalau memang masuk akal..ok()membuang alasannya:Ok(2)jadiSome(2).
Semua method ini juga ada di Result: map mengubah isi Ok, and_then menyambung langkah yang bisa gagal, unwrap_or memberi default.
unwrap dan expect
Ini kode tebakan tadi, dengan harga di atas:
fn main() {
println!("Es teh: {}", harga("es teh").unwrap());
let kopi = harga("kopi").unwrap();
println!("Kopi: {}", kopi);
}
Es teh: 5000
thread 'main' (25517591) panicked at src/main.rs:12:30:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Lolos compile, Es teh: 5000 tercetak, lalu program panic (berhenti seketika) di unwrap() yang kedua. Itu jawaban tebakannya. unwrap() artinya "aku yakin isinya ada": pada Some(42) ia memberi 42, pada Ok(2) ia memberi 2, tapi pada None atau Err ia menghentikan program. Baris Kopi: gak pernah tercetak, dan cargo run keluar dengan kode 101. Angka di dalam kurung setelah 'main' adalah nomor thread, beda di tiap jalan.
expect sama dengan unwrap, tapi pesannya kamu tulis sendiri:
fn main() {
let porsi: u32 = "dua".parse().expect("porsi harus angka");
println!("{}", porsi);
}
thread 'main' (25518653) panicked at src/main.rs:2:36:
porsi harus angka: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Kenapa unwrap di kode production dianggap bau? Bayangin aplikasi kasir yang crash untuk semua orang cuma karena satu pembeli salah ketik porsi. unwrap wajar di contoh kecil, di test, dan di prototipe. Di kode sungguhan, tangani None dan Err dengan match, if let, atau method di atas. Kalau kamu benar-benar yakin sesuatu gak mungkin gagal, pakai expect dengan pesan yang menjelaskan alasannya, supaya kalau ternyata salah, pesannya langsung menunjuk masalahnya.
panic atau Result?
Panic untuk bug: janji di dalam programmu sendiri yang dilanggar, misalnya index di luar batas atau data yang "pasti ada" ternyata gak ada. Program berhenti, karena melanjutkan dengan data yang salah lebih berbahaya. Result untuk kegagalan yang wajar terjadi dari luar: isian pengguna, file yang hilang, jaringan yang putus. Pertanyaan pegangannya: bisakah pemanggil berbuat sesuatu yang masuk akal kalau ini gagal? Kalau bisa, kembalikan Result.
⚠️ Jebakan yang sering ketemu
unwrap()di mana-mana. Lolos compile, lalu panic saat isiannya aneh. TanganiNonedanErr, atau pakaiexpectdengan alasan.- Memperlakukan
Option<u32>sepertiu32.harga("es teh") == 5000ditolak (E0308),harga("es teh") + 2000juga (E0369cannot add `{integer}` to `Option<u32>`). Keluarkan isinya dulu. parse()tanpa tipe tujuan. E0284type annotations needed. Tulisparse::<u32>().- Default yang menyembunyikan masalah.
unwrap_or(0)membuat kopi jadi gratis danunwrap_or(1)membuat "dua" jadi satu porsi. Pilih default yang benar-benar masuk akal, atau laporkan error-nya.
TL;DR: Option<T> itu Some(nilai) atau None, untuk nilai yang mungkin gak ada (harga("kopi")). Result<T, E> itu Ok(nilai) atau Err(alasan), untuk pekerjaan yang bisa gagal ("dua".parse::<u32>() memberi Err(ParseIntError { kind: InvalidDigit }), yang dicetak {} jadi invalid digit found in string). Tangani dengan match atau if let, atau olah dengan unwrap_or, map, and_then, ok_or, dan ok. unwrap() memberi isinya (Some(42).unwrap() jadi 42) tapi panic pada None atau Err; expect("...") sama, dengan pesanmu sendiri. Panic untuk bug, Result untuk kegagalan yang wajar dari luar.