The ? operator
Ada cara yang lebih pendek untuk menghandle Result
(dan Option
), lebih pendek daripada match
dan bahkan lebih pendek daripada if let
. Ia biasa disebut sebagai "question mark operator", dan Anda benar-benar hanya cukup menuliskan ?
. Setelah menuliskan function yang me-return sebuah result, Anda bisa menambahkan ?
. Ia akan melakukan:
- me-return apa yang ada di dalam
Result
jika iaOk
- Lewati saja jika ia
Err
Dengan kata lain, ?
melakukan hampir segalanya untuk Anda.
Kita bisa mencoba menggunakannya menggunakan .parse()
. Kita akan menuliskan sebuah function bernama parse_str
yang mana akan mencoba mengubah &str
menjadi i32
. Codenya seperti ini:
use std::num::ParseIntError; fn parse_str(input: &str) -> Result<i32, ParseIntError> { let parsed_number = input.parse::<i32>()?; // Disinilah `?` digunakan Ok(parsed_number) } fn main() {}
Function ini akan mengambil &str
. Jika ia Ok
, ia akan mengembalikan i32
yang terbungkus dengan Ok
. Jika ia adalah Err
, ia akan mengembalikan ParseIntError
. Kemudian kita mencoba untuk melakukan parsing ke angka, dan menggunakan ?
. Yang artinya "periksa apakah ia sebuah error, dan berikan kami hasilnya jika isinya aman". Jika status kembaliannya tidak Ok, ia akan mengembalikan error dan selesai. Tapi jika Ok, ia akan ke baris yang selanjutnya. Pada baris selanjutnya adalah angka yang terbungkus dengan Ok()
. Kita perlu membungkusnya dengan Ok
karena returnnya adalah Result<i32, ParseIntError>
, bukan i32
.
Sekarang, kita bisa menggunakan function yang telah kita buat. Mari kita lihat apa yang ia lakukan terhadap vec dengan element &str
.
fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> { let parsed_number = input.parse::<i32>()?; Ok(parsed_number) } fn main() { let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"]; for item in str_vec { let parsed = parse_str(item); println!("{:?}", parsed); } }
Hasilnya adalah:
Err(ParseIntError { kind: InvalidDigit })
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(6060)
Bagaimana kita menemukan std::num::ParseIntError
? Salah satu cara termudahnya adalah dengan cara "menanyakannya" kepada compiler.
fn main() { let failure = "Not a number".parse::<i32>(); failure.rbrbrb(); // ⚠️ Compiler: "Apa itu rbrbrb()???" }
Compiler tidak memahami code tersebut, dan mengatakan:
error[E0599]: no method named `rbrbrb` found for enum `std::result::Result<i32, std::num::ParseIntError>` in the current scope
--> src\main.rs:3:13
|
3 | failure.rbrbrb();
| ^^^^^^ method not found in `std::result::Result<i32, std::num::ParseIntError>`
Jadinya std::result::Result<i32, std::num::ParseIntError>
adalah signature yang kita perlukan.
Kita tidak perlu menuliskan std::result::Result
karena Result
selalu "in scope" (in scope = siap untuk digunakan / siap pakai). Rust melakukan ini ke semua type yang sering kita gunakan, sehingga kita tidak perlu lagi menulis std::result::Result
, std::collections::Vec
, dan seterusnya.
Kita belum berurusan dengan program yang mengharuskan kita menghandle file, oleh karena itu operator ?
belum terlalu terlihat berguna untuk sekarang ini. Namun ini adalah contoh singkat (yang sedikit kurang berguna) yang menunjukkan bagaimana Anda bisa menggunakannya dalam satu baris. Alih-alih hanya membuati32
menggunakan .parse()
, justru kita melakukan hal yang agak lebih rumit lagi. Kita membuatnya ke u16
, dan mengubahnya lagi ke String
, kemudian diubah ke u32
, kemudian ke String
lagi, dan akhirnya diubah ke i32
.
use std::num::ParseIntError; fn parse_str(input: &str) -> Result<i32, ParseIntError> { let parsed_number = input.parse::<u16>()?.to_string().parse::<u32>()?.to_string().parse::<i32>()?; // Tambahkan ? setiap mengecek dan pass ke method selanjutnya Ok(parsed_number) } fn main() { let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"]; for item in str_vec { let parsed = parse_str(item); println!("{:?}", parsed); } }
Program ini juga mencetak hal yang sama, tapi untuk yang ini kita meng-handle tiga Result
dalam satu baris. Nantinya kita akan menggunakan cara ini saat berurusan dengan file, karena hal yang berurusan dengan file akan selalu me-return Result
karena banyak sekali kemungkinan untuk melakukan kesalahan.
Bayangkan hal ini: Anda ingin membuka sebuah file, menulis sesuatu di file tersebut, dan menutupnya. Pertama-tama, Anda perlu berhasil menemukan filenya (tentunya ini memerlukan Result
). Kemudian Anda harus berhasil menuliskan sesuatu didalamnya (dan ini juga Result
). Dengan ?
Anda bisa melakukannya dengan satu baris saja.
When panic and unwrap are good
Rust memiliki macro panic!
yang mana bisa Anda gunakan untuk membuat panic. Ia cukup mudah untuk digunakan:
fn main() { panic!("Time to panic!"); }
Pesan "Time to panic!"
muncul saat Anda menjalankan programnya: thread 'main' panicked at 'Time to panic!', src\main.rs:2:3
Anda akan mengingat bahwa src\main.rs
adalah nama folder dan file, dan 2:3
adalah baris and kolom. Dengan informasi ini, Anda bisa menemukan codenya dan memperbaikinya.
panic!
adalah macro yang baik digunakan untuk memastikan bahwa Anda tahu bahwa ada sesuatu yang berubah. Contohnya, function yang bernama prints_three_things
selalu mencetak index [0], [1], dan [2] dari sebuah vector. Semuanya baik-baik saja karena kita selalu memberikan vector dengan tiga item:
fn prints_three_things(vector: Vec<i32>) { println!("{}, {}, {}", vector[0], vector[1], vector[2]); } fn main() { let my_vec = vec![8, 9, 10]; prints_three_things(my_vec); }
Program tersebut akan mencetak 8, 9, 10
dan semuanya baik-baik saja.
Tapi bayangkan bahwa kemudian kita menulis code menjadi lebih panjang, dan melupakan bahwa my_vec
tepatnya hanya untuk vec dengan panjang 3 element. Sekarang my_vec
pada code dibawah ini memiliki 6 element:
fn prints_three_things(vector: Vec<i32>) { println!("{}, {}, {}", vector[0], vector[1], vector[2]); } fn main() { let my_vec = vec![8, 9, 10, 10, 55, 99]; // Sekarang my_vec memiliki 6 element prints_three_things(my_vec); }
Tentu saja tidak terjadi errorapapun, karena [0] dan [1] dan [2] mampu dicapai oleh Vec
yang lebih panjang. Namun bagaimana jika menjadi sangat penting untuk tetap menggunakan Vec
dengan panjang tiga elemen? Kita pun tidak tahu bahwa ini adalah sebuah masalah karena compiler pun tidak memberikan panic. Untuk menyelesaikan problem ini, kita harus melakukan hal seperti berikut:
fn prints_three_things(vector: Vec<i32>) { if vector.len() != 3 { panic!("my_vec must always have three items") // akan panic jika panjang elementnya bukan 3 } println!("{}, {}, {}", vector[0], vector[1], vector[2]); } fn main() { let my_vec = vec![8, 9, 10]; prints_three_things(my_vec); }
Sekarang kita akan tahu apabila vectornya memiliki panjang 6 element karena ia akan memberikan pesan panic seperti yang kita tuliskan:
// ⚠️ fn prints_three_things(vector: Vec<i32>) { if vector.len() != 3 { panic!("my_vec must always have three items") } println!("{}, {}, {}", vector[0], vector[1], vector[2]); } fn main() { let my_vec = vec![8, 9, 10, 10, 55, 99]; prints_three_things(my_vec); }
Pesan yang diberikan oleh program tersebut adlaah thread 'main' panicked at 'my_vec must always have three items', src\main.rs:8:9
. Bersyukurlah dengan adanya panic!
, sekarang kita mengingat bahwa my_vec
seharusnya hanya berisi tiga element saja. Jadinya panic!
adalah macro yang baik untuk membuat pengingat pada code yang Anda buat.
Ada 3 macro lainnya yang mirip dengan panic!
yang akan sering Anda gunakan pada saat melakukan testing. Mereka adalah: assert!
, assert_eq!
, dan assert_ne!
.
Berikut adalah penjelasannya:
assert!()
: jika sesuatu yang berada di dalam()
bukanlah true, program akan panic.assert_eq!()
: 2 buah item di dalam()
harus sama.assert_ne!()
: 2 buah item di dalam()
harus tidak sama. (ne artinya not equal / tidak sama)
Contohnya:
fn main() { let my_name = "Loki Laufeyson"; assert!(my_name == "Loki Laufeyson"); assert_eq!(my_name, "Loki Laufeyson"); assert_ne!(my_name, "Mithridates"); }
Program di atas tidak akan melakukan apapun (atau menampilkan apapun), karena semua macro assertnya sesuai. (Inilah yang kita inginkan)
Anda juga bisa menambahkan pesan jika Anda menginginkannya.
fn main() { let my_name = "Loki Laufeyson"; assert!( my_name == "Loki Laufeyson", "{} should be Loki Laufeyson", my_name ); assert_eq!( my_name, "Loki Laufeyson", "{} and Loki Laufeyson should be equal", my_name ); assert_ne!( my_name, "Mithridates", "You entered {}. Input must not equal Mithridates", my_name ); }
Pesan yang ditulis tersebut hanya akan muncul apabila programnya panic. Sehingga jika Anda menjalankan ini:
fn main() { let my_name = "Mithridates"; assert_ne!( my_name, "Mithridates", "You enter {}. Input must not equal Mithridates", my_name ); }
Ia akan menampilkan:
thread 'main' panicked at 'assertion failed: `(left != right)`
left: `"Mithridates"`,
right: `"Mithridates"`: You entered Mithridates. Input must not equal Mithridates', src\main.rs:4:5
Jadi, compiler mengatakan "Kamu bilang bahwa kiri != kanan, tapi hasilnya justru kiri == kanan". Dan ia memunculkan pesan yang telah kita tuliskan, You entered Mithridates. Input must not equal Mithridates
.
unwrap
juga bagus di saat Anda menulis code dan membuatnya crash apabila ada problem pada code tersebut. Kemudian, di saat code yang Anda tulis telah selesai, ada baiknya mengubah unwrap
ke sesuatu yang lainnya yang tidak menybabkan crash pada program.
Anda juga bisa menggunakan expect
, yang mana mirip seperti unwrap
, namun agak lebih baik, karena Anda bisa memnentukan sendiri apa pesan errornya. Textbooks pun biasanya memberika saran seperti ini: "Jika Anda banyak menggunakan .unwrap()
, setidaknya gunakan .expect()
untuk pesan error yang lebih baik."
Program di bawah ini akan crash:
// ⚠️ fn get_fourth(input: &Vec<i32>) -> i32 { let fourth = input.get(3).unwrap(); *fourth } fn main() { let my_vec = vec![9, 0, 10]; let fourth = get_fourth(&my_vec); }
Pesan errornya adalah thread 'main' panicked at 'called Option::unwrap() on a None value', src\main.rs:7:18
.
Sekarang kita tuliskan pesannya menggunakan expect
:
// ⚠️ fn get_fourth(input: &Vec<i32>) -> i32 { let fourth = input.get(3).expect("Input vector needs at least 4 items"); *fourth } fn main() { let my_vec = vec![9, 0, 10]; let fourth = get_fourth(&my_vec); }
Tentu saja program yang inipun akan crash juga, namun dengan pesan error yang lebih baik: thread 'main' panicked at 'Input vector needs at least 4 items', src\main.rs:7:18
. .expect()
sedikit lebih baik daripada .unwrap()
karena hal ini, tapi ia akan tetap panic saat membuka None
. Sekarang, kita akan mencontohkan sebuah bad practice, yaitu function yang mencoba untuk melakukan unwrap dua kali. Ia mengambil Vec<Option<i32>>
sebagai parameter, sehingga mungkin beberapa bagiannya akan menghasilkan Some<i32>
atau juga None
.
fn try_two_unwraps(input: Vec<Option<i32>>) { println!("Index 0 is: {}", input[0].unwrap()); println!("Index 1 is: {}", input[1].unwrap()); } fn main() { let vector = vec![None, Some(1000)]; // Vector ini menghasilkan None, sehingga ia akan panic try_two_unwraps(vector); }
Pesan errornya adalah: thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src\main.rs:2:32
. Kita tidak yakin apakah errornya muncul dari .unwrap()
yang pertama ataukah .unwrap()
yang kedua sampai kita memeriksa baris codenya. Ini akan lebih baik apabila kita melakukan pengecekan terhadap panjang vectornya dan juga tidak melakukannya dengan unwrap. Dengan .expect()
setidaknya program kita akan menjadi sedikit lebih baik. Inilah contoh codenya apabila kita menggunakan .expect()
:
fn try_two_unwraps(input: Vec<Option<i32>>) { println!("Index 0 is: {}", input[0].expect("The first unwrap had a None!")); println!("Index 1 is: {}", input[1].expect("The second unwrap had a None!")); } fn main() { let vector = vec![None, Some(1000)]; try_two_unwraps(vector); }
Sehingga errornya menjadi lebih baik: thread 'main' panicked at 'The first unwrap had a None!', src\main.rs:2:32
. Kita diberikan informasi tentang unwrap yang mana yang menghasilkan error sehingga kita bisa dengan mudah menemukannya dan memperbaikinya.
Anda juga bisa menggunakan unwrap_or
jika Anda ingin selalu memiliki nilai yang ingin Anda pilih. Jika Anda melakukan ini maka ia tidak akan pernah panic. Yang perlu dicatat adalah:
-
- Ini bagus karena program menjadi tidak akan panic, namun
-
- Mungkin saja tidak bagus apabila Anda menginginkan program menjadi panic jika terdapat problem pada code tersebut.
Tapi biasanya kita tidak ingin program yang kita buat menjadi panic, jadinya unwrap_or
adalah cara yang baik untuk digunakan.
fn main() { let my_vec = vec![8, 9, 10]; let fourth = my_vec.get(3).unwrap_or(&0); // Jika .get tidak bekerja, kita akan membuat value &0. // .get mengembalikan sebuah reference, sehingga kita perlu &0, bukan 0 println!("{}", fourth); // Anda bisa menulis "*fourth" jika Anda ingin fourth menjadi // 0 dan bukan &0, tapi disini kita hanya mencetaknya saja, sehingga hal itu tidaklah penting }
Ia akan mencetak 0
karena .unwrap_or(&0)
memberikan 0 meskipun .get()
memberikan None
.