Option and Result
Kita telah mengerti enums dan generics, sehingga kita bisa mengerti Option
dan Result
. Rust menggunakan 2 enum ini dengan tujuan untuk membuat code menjadi lebih safe.
Kita mulai dengan Option
.
Option
Kita menggunakan Option
apabila kita memiliki sebuah value yang mungkin saja ada (exist), atau bisa juga tidak. Di saat sebuah value exist, ia akan mengembalikan Some(value)
dan jika tidak, maka ia mengembalikan None
. Ini adalah contoh dari code yang buruk yang nantinya bisa kita perbaiki dengan menggunakan Option
.
// ⚠️ fn take_fifth(value: Vec<i32>) -> i32 { value[4] } fn main() { let new_vec = vec![1, 2]; let index = take_fifth(new_vec); }
Di saat kita menjalankan codenya, ia akan menunjukkan pesan "panics". Berikut adalah pesannya:
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 4', src\main.rs:34:5
Panic artinya adalah program berhenti sebelum problemnya terjadi. Rust melihat bahwa function menginginkan sesuatu yang tidak mungkin untuk dilakukan (impossible), dan menghentikannya. Problem yang dimaksud disini adalah "unwinds the stack" (mengambil value dari stack) dan memberitahu Anda, "Maaf, aku tidak bisa melakukannya".
Jadi sekarang kita akan mengubah type kembaliannya dari i32
ke Option<i32>
. Ini berarti "Beri aku Some(i32)
, jika ada valuenya. Dan beri aku None
jika tidak ada valuenya". Kita bisa juga menyebut bahwa i32
"wrapped"/"dibungkus" didalam Option
, yang berarti bahwa ia berada didalam Option
. Anda perlu melakukan sesuatu untuk mendapatkan valuenya.
fn take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { // .len() akan memberikan panjang dari sebuah vec. // Dan setidaknya, panjangnya haruslah 5. None } else { Some(value[4]) } } fn main() { let new_vec = vec![1, 2]; let bigger_vec = vec![1, 2, 3, 4, 5]; println!("{:?}, {:?}", take_fifth(new_vec), take_fifth(bigger_vec)); }
Hasilnya adalah None, Some(5)
. Ini adalah hal yang baik, karena sekarang compiler tidak lagi panic. Tapi bagaimana caranya ia mendapatkan value 5 dari Some(5)
?
Kita bisa mengembil value di dalam Option menggunakan .unwrap()
, namun berhati-hatilah dengan .unwrap()
. Itu sama seperti melakukan unwrapping/membuka bungkus dari sebuah kotak yang kita tidak tahu apa isinya: mungkin saja isinya adalah sesuatu yang baik, atau mungkin saja isinya adalah belasan ekor ular. Anda sebaiknya hanya menggunakan .unwrap()
di saat Anda yakin bahwa "kotak" yang Anda ingin buka tidaklah berbahaya. Jika Anda melakukan unwrap pada value None
, maka program akan panic.
// ⚠️ fn take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { None } else { Some(value[4]) } } fn main() { let new_vec = vec![1, 2]; let bigger_vec = vec![1, 2, 3, 4, 5]; println!("{:?}, {:?}", take_fifth(new_vec).unwrap(), // yang satu ini adalah None. .unwrap() akan membuat program panic! take_fifth(bigger_vec).unwrap() ); }
Berikut adalah pesan panicnya:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src\main.rs:14:9
Tapi kita tidak harus menggunakan .unwrap()
. Kita juga bisa menggunakan match
. Kemudian kita bisa mencetak value dari Option yang returnya adalah Some
, dan sama sekali tidak menyentuh yang None
. Berikut contohnya:
fn take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { None } else { Some(value[4]) } } fn handle_option(my_option: Vec<Option<i32>>) { for item in my_option { match item { Some(number) => println!("Found a {}!", number), None => println!("Found a None!"), } } } fn main() { let new_vec = vec![1, 2]; let bigger_vec = vec![1, 2, 3, 4, 5]; let mut option_vec = Vec::new(); // Buat vec baru untuk menyimpan option // Type dari vec tersebut adalah: Vec<Option<i32>>. Yang artinya adalah vec dari Option<i32>. option_vec.push(take_fifth(new_vec)); // Ini akan melakukan push value "None" ke dalam vec option_vec.push(take_fifth(bigger_vec)); // Ini akan melakukan push value "Some(5)" ke dalam vec handle_option(option_vec); // handle_option akan memeriksa setiap option yang berada di dalam vec. // Ia akan mencetak value jika itu adalah Some. Dan sama sekali tidak disentuh apabila itu adalah None. }
Hasil cetaknya adalah:
Found a None!
Found a 5!
Karena kita telah mengetahui generics, maka kita bisa membaca code dengan Option
dibawah ini. Berikut codenya:
enum Option<T> { None, Some(T), } fn main() {}
Poin penting yang harus diingat: dengan Some
, Anda memiliki value dengan type T
(type apapun). Juga dicatat, bahwa angle bracket setelah nama enum
(yang mengurung T
) memberitahukan compiler bahwa ia generic. Ia tidak memiliki trait seperti Display
atau apapun yang membatasinya, sehingga ia bisa bertype apapun. Tapi, dengan None
, Anda tidak mendapatkan apapun.
Jadinya, di dalam match
statement untuk Option, Anda tidak bisa menuliskan:
#![allow(unused)] fn main() { // 🚧 Some(value) => println!("The value is {}", value), None(value) => println!("The value is {}", value), }
karena None
tetaplah None
.
Tentu saja, ada cara yang lebih mudah untuk menggunakan Option. Pada code ini, kita akan menggunakan sebuah method bernama .is_some()
untuk memberi tahu kita jika ia adalah Some
. (Ya, ada juga method dengan nama .is_none()
.) Dengan cara yang lebih mudah ini, kita tidak lagi memerlukan function handle_option()
. Kita juga tidak memerlukan sebuah vec untuk menyimpan value dari Option.
fn take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { None } else { Some(value[4]) } } fn main() { let new_vec = vec![1, 2]; let bigger_vec = vec![1, 2, 3, 4, 5]; let vec_of_vecs = vec![new_vec, bigger_vec]; for vec in vec_of_vecs { let inside_number = take_fifth(vec); if inside_number.is_some() { // .is_some() mengembalikan true jika kita mendapatkan Some, false jika kita mendapatkan None println!("We got: {}", inside_number.unwrap()); // tentunya akan aman menggunakan .unwrap() karena kita telah melakukan pemeriksaan } else { println!("We got nothing."); } } }
Ini adalah hasilnya:
We got nothing.
We got: 5
Result
Result mirip dengan Option, namun ini adalah perbedaannya:
- Option berurusan tentang
Some
atauNone
(ada valuenya atau tidak ada valuenya), - Result berurusan tentang
Ok
atauErr
(hasilnya sesuai, atau menghasilkan error).
Jadi Option
itu seakan kita berpikir: "Mungkin ada sesuatu, dan mungkin juga tidak ada.". Sedangkan Result
itu seakan kita berpikir: "Mungkin ini akan error/gagal."
Untuk membandingkannya, ini adalah perbedaan antara Option dan Result.
enum Option<T> { None, Some(T), } enum Result<T, E> { Ok(T), Err(E), } fn main() {}
Result memiliki value didalam Ok
, dan value didalam Err
. Ini dikarenakan errors biasanya berisi informasi yang mendeskripsikan errornya.
Result<T, E>
artinya Anda perlu untuk memikirkan apa yang ingin Anda return di saat ia Ok
, dan apa yang ingin Anda return di saat ia Err
. Sebenarnya, Anda bebas untuk menentukan apapun, bahkan melakukan hal yang seperti dilakukan pada contoh di bawah ini pun:
fn check_error() -> Result<(), ()> { Ok(()) } fn main() { check_error(); }
check_error
mengatakan "return ()
jika kita mendapatkan Ok
, dan return ()
jika kita mendapatkan Err
". Maka, kita me-return Ok
dengan ()
.
Compiler akan memberikan kita warning yang menarik:
warning: unused `std::result::Result` that must be used
--> src\main.rs:6:5
|
6 | check_error();
| ^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
Ini benar: kita hanya mengembalikan Result
tapi bisa jadi itu adalah Err
. Maka, kita perlu tangani errornya , meskipun kita tidak melakukan apapun.
fn give_result(input: i32) -> Result<(), ()> { if input % 2 == 0 { return Ok(()) } else { return Err(()) } } fn main() { if give_result(5).is_ok() { println!("It's okay, guys") } else { println!("It's an error, guys") } }
Code di atas akan mencetak It's an error, guys
. Jadinya, kita telah menangani error pertama kita pada Result.
Diingat, ada empat method untuk melakukan pengecekan secara mudah, yaitu .is_some()
, is_none()
, is_ok()
, dan is_err()
.
Terkadang sebuah function dengan Result akan menggunakan String
untuk Err
valuenya. Ini bukanlah cara terbaik untuk digunakan, namun ini adalah sedikit lebih baik daripada apa yang sejauh ini sudah kita lakukan.
fn check_if_five(number: i32) -> Result<i32, String> { match number { 5 => Ok(number), _ => Err("Sorry, the number wasn't five.".to_string()), // Ini adalah pesan error yang kita buat } } fn main() { let mut result_vec = Vec::new(); // Buat vec baru untuk resultnya for number in 2..7 { result_vec.push(check_if_five(number)); // push setiap result ke dalam vec } println!("{:?}", result_vec); }
Berikut adalah isi dari vec tersebut:
[Err("Sorry, the number wasn\'t five."), Err("Sorry, the number wasn\'t five."), Err("Sorry, the number wasn\'t five."), Ok(5),
Err("Sorry, the number wasn\'t five.")]
Sama seperti Option, melakukan .unwrap()
pada Err
akan menyebabkan panic.
// ⚠️ fn main() { let error_value: Result<i32, &str> = Err("There was an error"); // Buat sebuah Result yang valuenya adalah Err println!("{}", error_value.unwrap()); // lakukan unwrap }
Hasilnya, program panics, dan mencetak:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "There was an error"', src\main.rs:30:20
Informasi ini membantu kita untuk memperbaiki code. src\main.rs:30:20
artinya "di dalam main.rs di direktori src, pada line 30 dan column 20". Sehinggan Anda bisa ke bagian tersebut untuk melihat codenya dan memperbaiki masalahnya.
Anda juga bisa membuat type error Anda sendiri. Function Result pada standard library dan code Rust yang ditulis oleh orang lain biasanya menggunakan ini. Contohnya adalah function dari standard library ini:
#![allow(unused)] fn main() { // 🚧 pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error> }
Function ini mengambil byte dari vector (u8
) dan mencoba untuk membuatnya menjadi String
. Maka Ok-case pada Result adalah String
dan Error-casenya adalah FromUtf8Error
. Anda dapat memberikan error type Anda menggunakan nama apa pun yang Anda inginkan.
Menggunakan match
dengan Option
dan Result
terkadang membutuhkan code yang lebih panjang. Contohnya, method .get()
mengembalikan Option
pada Vec
.
fn main() { let my_vec = vec![2, 3, 4]; let get_one = my_vec.get(0); // 0 untuk mengambil angka pertama let get_two = my_vec.get(10); // Return None println!("{:?}", get_one); println!("{:?}", get_two); }
Hasilnya adalah:
Some(2)
None
Jadi sekarang kita bisa menggunakan match untuk mendapatkan valuenya. Mari kita gunakan range dari 0 sampai dengan 10 untuk melihat apakah ia cocok dengan angka-angka di dalam my_vec
.
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { match my_vec.get(index) { Some(number) => println!("The number is: {}", number), None => {} } } }
Ini bagus, namun kita tidak melakukan apapun pada kondisi None
karen kita tidak peduli pada case itu. Nah, kita bisa membuat codenya menjadi lebih singkat dengan menggunakan if let
. if let
artinya "lakukan sesuatu jika ia cocok, dan tidak usah lakukan apapun jika tidak cocok". if let
digunakan apabila Anda tidak merasa perlu untuk menuliskan apa yang harus dilakukan pada semua kondisi matching.
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { if let Some(number) = my_vec.get(index) { println!("The number is: {}", number); } } }
Yang perlu untuk diingat: if let Some(number) = my_vec.get(index)
artinya "Jika Anda mendapat Some(number)
dari my_vec.get(index)
".
Juga perlu dicatat: ia menggunakan =
. Ini bukanlah boolean.
while let
bisa dikatakan sebagai while loop yang dimodifikasi dengan if let
. Anggap saja kita memiliki data dari stasiun cuaca seperti ini:
["Berlin", "cloudy", "5", "-7", "78"]
["Athens", "sunny", "not humid", "20", "10", "50"]
Kita ingin mendapatkan angkanya saja, dan tidak menginginkan kata-kata yang berada di dalamnya . Untuk mengambil angkanya, kita bisa menggunakan method bernama parse::<i32>()
. parse()
adalah method, and ::<i32>
adalah typenya. Ia akan mencoba untuk mengubah &str
menjadi i32
, dan akan memberikannya ke kita jika memang bisa dilakukan. Ia akan mengembalikan Result
, karena mungkin saja ia gagal melakukannya (misalnya, Anda ingin melakukan parse pada "Billybrobby", dan tentu saja akan gagal - karena ia bukan angka).
Kita juga akan menggunakan .pop()
. Ia akan mengambil item terakhir pada sebuah vector.
fn main() { let weather_vec = vec![ vec!["Berlin", "cloudy", "5", "-7", "78"], vec!["Athens", "sunny", "not humid", "20", "10", "50"], ]; for mut city in weather_vec { println!("For the city of {}:", city[0]); // Di dalam data kita, setia item pertama adalah nama kota while let Some(information) = city.pop() { // Ini berarti: tetap jalankan instruksi di dalam while sampai pada kondisi dimana tidak bisa melakukan pop lagi // Di saat vector mencapai pada 0 item, ia akan me-return None // dan ia akan berhenti. if let Ok(number) = information.parse::<i32>() { // Mencoba untuk melakukan parse pada variabel yang kita namakan information // Ia akan mengembalikan Result. Jika hasilnya adalah Ok(number), ia akan mencetaknya println!("The number is: {}", number); } // Kita tidak menulis apapun pada bagian ini karena kita tidak melakukan apapun jika mendapatkan error. // Dengan kata lain, kita throw/buang semua error tersebut } } }
Hasilnya adalah sebagai berikut:
For the city of Berlin:
The number is: 78
The number is: -7
The number is: 5
For the city of Athens:
The number is: 50
The number is: 10
The number is: 20