Error Recoverable pake Result
Sebagian besar error nggak terlalu serius sampe harus ngehentiin program sepenuhnya. Kadang pas sebuah fungsi gagal itu karena alasan yang bisa kita interpretasi dan respon dengan gampang. Misalnya, kalau kita nyoba buka file dan operasi itu gagal gara-gara filenya nggak ada, kita mungkin mau bikin file itu bukannya malah nge-terminate (menghentikan) prosesnya.
Inget dari “Menangani Potensi Kegagalan dengan Result” di
Bab 2 kalau enum Result didefinisikan punya dua varian, Ok sama Err,
kayak gini:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T sama E itu generic type parameters (parameter tipe generik): kita bakal
bahas generik lebih detail di Bab 10. Yang perlu kita tau sekarang adalah T
merepresentasikan tipe dari nilai yang bakal dibalikin di kasus sukses di dalem
varian Ok, dan E merepresentasikan tipe dari error yang bakal dibalikin di
kasus gagal di dalem varian Err. Karena Result punya generic type
parameters ini, kita bisa pake tipe Result dan fungsi-fungsi yang
didefinisikan padanya di banyak situasi yang beda di mana nilai sukses dan nilai
error yang mau kita balikin mungkin beda-beda.
Yuk kita manggil fungsi yang balikin nilai Result karena fungsinya bisa aja
gagal. Di Listing 9-3 kita nyoba ngebuka sebuah file.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
Tipe kembalian (return type) dari File::open adalah Result<T, E>. Parameter
generik T udah diisi sama implementasi dari File::open dengan tipe dari
nilai suksesnya, yaitu std::fs::File, yang merupakan sebuah file handle.
Tipe dari E yang dipake di nilai error adalah std::io::Error. Tipe kembalian
ini artinya pemanggilan ke File::open bisa aja sukses dan balikin file
handle yang bisa kita baca atau tulis. Pemanggilan fungsi ini juga bisa aja
gagal: misalnya, filenya mungkin nggak ada, atau kita mungkin nggak punya izin
(permission) buat akses file itu. Fungsi File::open butuh cara buat ngasih
tau kita apakah dia sukses atau gagal dan di saat yang sama ngasih kita antara
file handle atau informasi error. Informasi ini persis apa yang disampein
sama enum Result.
Di kasus di mana File::open sukses, nilai di variabel greeting_file_result
bakal jadi instance dari Ok yang nampung file handle. Di kasus di mana dia
gagal, nilai di greeting_file_result bakal jadi instance dari Err yang
nampung lebih banyak info soal jenis error yang terjadi.
Kita perlu nambahin kode di Listing 9-3 buat ngambil tindakan yang beda
tergantung dari nilai yang dibalikin sama File::open. Listing 9-4 nunjukin
salah satu cara buat nanganin Result pake tool dasar, yaitu ekspresi match
yang udah kita bahas di Bab 6.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
match buat nanganin varian Result yang mungkin dibalikinPerhatiin ya kalau sama kayak enum Option, enum Result sama varian-variannya
udah dibawa ke dalem scope lewat prelude, jadi kita nggak perlu nentuin
Result:: sebelum varian Ok sama Err di arms dari match-nya.
Pas hasilnya Ok, kode ini bakal balikin nilai file di dalem varian Ok-nya,
dan terus kita nge-assign nilai file handle itu ke variabel greeting_file.
Setelah match, kita bisa pake file handle-nya buat baca atau nulis.
Arm lain dari match-nya nanganin kasus di mana kita dapet nilai Err dari
File::open. Di contoh ini, kita milih buat manggil macro panic!. Kalau
nggak ada file namanya hello.txt di direktori kita saat ini dan kita jalanin
kode ini, kita bakal liat output berikut dari macro panic!:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Kayak biasa, output ini ngasih tau kita persis apa yang salah.
Nge-match di Error yang Beda-beda
Kode di Listing 9-4 bakal panic! nggak peduli apa alasan File::open gagal.
Padahal, kita mau ngambil tindakan yang beda buat alasan kegagalan yang beda
juga. Kalau File::open gagal gara-gara filenya nggak ada, kita mau bikin
file itu terus balikin handle ke file baru itu. Kalau File::open gagal buat
alasan apa pun lainnya—misalnya, karena kita nggak punya izin buat buka
filenya—kita tetep mau kodenya buat panic! dengan cara yang sama kayak di
Listing 9-4. Buat ini, kita nambahin ekspresi match bersarang (inner match),
yang ditunjukin di Listing 9-5.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Tipe dari nilai yang dibalikin sama File::open di dalem varian Err adalah
io::Error, yang merupakan struct yang disediain sama standard library.
Struct ini punya method kind yang bisa kita panggil buat dapet nilai
io::ErrorKind. Enum io::ErrorKind disediain sama standard library dan
punya varian-varian yang merepresentasikan berbagai jenis error yang mungkin
terjadi dari sebuah operasi io. Varian yang mau kita pake adalah
ErrorKind::NotFound, yang ngindikasikan kalau file yang lagi kita coba buka
itu belum ada. Jadi kita nge-match greeting_file_result, tapi kita juga punya
inner match di error.kind().
Kondisi yang mau kita cek di inner match adalah apakah nilai yang dibalikin
sama error.kind() itu adalah varian NotFound dari enum ErrorKind. Kalau
iya, kita nyoba bikin filenya pake File::create. Tapi, karena File::create
juga bisa aja gagal, kita butuh arm kedua di ekspresi inner match kita. Pas
filenya nggak bisa dibuat, pesan error yang beda bakal dicetak. Arm kedua dari
match bagian luar (outer match) tetep sama, jadi programnya bakal panic
buat error apa pun selain error file nggak ditemuin.
Alternatif Buat Penggunaan match dengan Result<T, E>
Banyak sekali ya match-nya! Ekspresi match itu sangat berguna tapi dia
juga lumayan primitif. Di Bab 13, kita bakal belajar soal closures, yang
dipake bareng banyak method yang didefinisikan pada Result<T, E>. Method-
method ini bisa lebih ringkas daripada pake match pas nanganin nilai
Result<T, E> di kode kita.
Misalnya, ini cara lain buat nulis logika yang sama kayak yang ditunjukin di
Listing 9-5, kali ini pake closures dan method unwrap_or_else:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Masalah pas bikin file: {error:?}");
})
} else {
panic!("Masalah pas buka file: {error:?}");
}
});
}
Walaupun kode ini punya perilaku yang sama kayak Listing 9-5, dia nggak punya
ekspresi match apa pun dan lebih bersih buat dibaca. Balik lagi ke contoh
ini setelah kita kelar baca Bab 13, terus cek method unwrap_or_else di
dokumentasi standard library. Masih banyak lagi method kayak gini yang bisa
ngerapihin ekspresi match bersarang yang sangat besar pas kita lagi berurusan
sama error.
Jalan Pintas buat Panic Kalo Error: unwrap sama expect
Pake match emang jalan dengan baik sih, tapi bisa agak kepanjangan (verbose)
dan nggak selalu ngomunikasikan maksud kita dengan baik. Tipe Result<T, E>
punya banyak method pembantu (helper methods) yang didefinisikan padanya buat
ngelakuin berbagai tugas yang lebih spesifik. Method unwrap itu adalah method
shortcut (jalan pintas) yang diimplementasikan persis kayak ekspresi match
yang kita tulis di Listing 9-4. Kalau nilai Result-nya adalah varian Ok,
unwrap bakal balikin nilai di dalem Ok-nya. Kalau Result-nya adalah
varian Err, unwrap bakal manggil macro panic! buat kita. Ini contoh
penggunaan unwrap:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
Kalau kita jalanin kode ini tanpa file hello.txt, kita bakal liat pesan error
dari pemanggilan panic! yang dilakuin sama method unwrap:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Sama halnya, method expect juga ngebolehin kita buat milih pesan error panic!-nya
sendiri. Pake expect bukannya unwrap dan ngasih pesan error yang bagus
bisa nyampein maksud kita dan bikin ngelacak sumber panic jadi lebih gampang.
Sintaks dari expect keliatan kayak gini:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Kita pake expect dengan cara yang sama kayak unwrap: buat balikin file
handle-nya atau manggil macro panic!. Pesan error yang dipake sama expect
di pemanggilan panic!-nya bakal jadi parameter yang kita masukin ke expect,
bukannya pesan panic! default yang dipake sama unwrap. Ini contoh outputnya:
thread 'main' panicked at src/main.rs:5:10:
hello.txt harusnya udah disertain di project ini: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Di production-quality code (kode level produksi), kebanyakan Rustacean milih
expect daripada unwrap dan ngasih konteks lebih banyak soal kenapa operasi
itu diharapkan bakal selalu berhasil. Dengan gitu, kalau asumsi kita terbukti
salah, kita punya lebih banyak informasi yang bisa dipake pas debugging.
Ngelepar Balik Error (Propagating Errors)
Pas implementasi sebuah fungsi manggil sesuatu yang mungkin gagal, bukannya nanganin error-nya di dalem fungsi itu sendiri, kita bisa milih buat balikin error-nya ke kode yang manggil fungsi itu biar mereka yang mutusin mau ngapain. Ini dikenal sebagai propagating the error (ngelepar balik error) dan ngasih kontrol lebih banyak ke kode pemanggil, di mana mungkin ada lebih banyak informasi atau logika yang nentuin gimana error itu harus di-handle daripada apa yang tersedia di konteks fungsi kita.
Misalnya, Listing 9-6 nunjukin fungsi yang ngebaca username dari sebuah file. Kalau filenya nggak ada atau nggak bisa dibaca, fungsi ini bakal balikin error- error itu ke kode yang manggil fungsi ini.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
matchFungsi ini sebenernya bisa ditulis pake cara yang jauh lebih pendek, tapi kita
bakal mulai dengan ngelakuinnya secara manual biar bisa eksplor cara nanganin
error; nanti di akhir, kita bakal tunjukin cara yang lebih singkat. Yuk kita
liat tipe kembalian (return type) dari fungsinya dulu:
Result<String, io::Error>. Ini artinya fungsinya balikin nilai dari tipe
Result<T, E>, di mana parameter generik T udah diisi pake tipe konkret
String dan tipe generik E udah diisi pake tipe konkret io::Error.
Kalau fungsinya berhasil tanpa masalah apa pun, kode yang manggil fungsi ini
bakal nerima nilai Ok yang nampung sebuah String—yaitu username yang
dibaca sama fungsi ini dari file. Kalau fungsi ini nemu masalah apa pun, kode
pemanggil bakal nerima nilai Err yang nampung instance dari io::Error yang
isinya informasi lebih lanjut soal masalahnya. Kita milih io::Error sebagai
tipe kembalian dari fungsi ini karena kebetulan itu adalah tipe dari nilai error
yang dibalikin dari kedua operasi yang lagi kita panggil di dalem body fungsi ini
yang mungkin gagal: yaitu fungsi File::open sama method read_to_string.
Body dari fungsi ini dimulai dengan manggil fungsi File::open. Terus kita
nanganin nilai Result-nya pake match yang mirip kayak match di Listing 9-4.
Kalau File::open berhasil, file handle di variabel pattern file bakal jadi
nilai di variabel mutable username_file dan fungsinya lanjut. Di kasus Err,
bukannya manggil panic!, kita pake keyword return buat balik (return)
lebih awal dari fungsi sepenuhnya dan ngelepar nilai error dari File::open,
yang sekarang ada di variabel pattern e, balik ke kode pemanggil sebagai
nilai error dari fungsi ini.
Jadi, kalau kita punya file handle di username_file, fungsinya terus bikin
String baru di variabel username terus manggil method read_to_string pada
file handle di username_file buat baca isi filenya ke dalem username.
Method read_to_string juga balikin sebuah Result karena dia bisa aja gagal,
walaupun File::open tadi udah berhasil. Jadi kita butuh match satu lagi
buat nanganin Result itu: kalau read_to_string berhasil, berarti fungsi
kita udah berhasil, dan kita balikin username dari filenya yang sekarang udah
ada di variabel username yang dibungkus di dalem sebuah Ok. Kalau
read_to_string gagal, kita balikin nilai error-nya pake cara yang sama kayak
kita balikin nilai error di dalem match yang nanganin nilai kembalian dari
File::open. Tapi, kita nggak perlu secara eksplisit bilang return, karena ini
adalah ekspresi terakhir di fungsinya.
Kode yang manggil fungsi ini nanti bakal dapet antara nilai Ok yang isinya
sebuah username atau nilai Err yang isinya sebuah io::Error. Terserah kode
pemanggilnya mau ngapain sama nilai-nilai itu. Kalau kode pemanggilnya dapet
nilai Err, dia bisa aja manggil panic! terus nge-crash-in programnya,
pake username default, atau nyari username dari tempat lain selain dari file,
misalnya. Kita nggak punya informasi yang cukup soal apa yang sebenernya lagi
dicoba lakuin sama kode pemanggil, jadi kita nge-lempar balik semua informasi
sukses atau error ke atas biar di-handle sama dia dengan bener.
Pola ngelepar balik error ini saking umumnya di Rust sampe Rust nyediain
operator tanda tanya ? buat bikin proses ini lebih gampang.
Jalan Pintas Buat Ngelepar Error: Operator ?
Listing 9-7 nunjukin implementasi dari read_username_from_file yang punya
fungsionalitas yang sama kayak di Listing 9-6, tapi implementasi ini pake
operator ?.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
?Tanda ? yang ditaruh setelah sebuah nilai Result itu didefinisikan buat
kerja dengan cara yang hampir persis sama kayak ekspresi match yang kita
definisikan buat nanganin nilai Result di Listing 9-6. Kalau nilai Result-nya
adalah Ok, nilai di dalem Ok-nya bakal dibalikin dari ekspresi ini, dan
programnya bakal lanjut. Kalau nilainya adalah Err, Err itu bakal dibalikin
dari keseluruhan fungsi seolah-olah kita udah pake keyword return, jadi nilai
error-nya bakal dilempar balik ke kode pemanggil.
Ada sedikit perbedaan antara apa yang dilakuin ekspresi match di Listing 9-6
sama apa yang dilakuin operator ?: nilai error yang dikenain operator ?
bakal ngelewatin fungsi from, yang didefinisikan di trait From di standard
library, yang dipake buat nge-convert nilai dari satu tipe ke tipe lainnya.
Pas operator ? manggil fungsi from, tipe error yang diterima bakal di-convert
jadi tipe error yang didefinisikan di tipe kembalian (return type) dari fungsi
saat ini. Ini sangat berguna pas sebuah fungsi balikin satu tipe error kustom
buat merepresentasikan semua kemungkinan cara fungsi itu bisa gagal, walaupun
bagian-bagian di dalemnya mungkin gagal karena banyak alasan yang beda.
Misalnya, kita bisa ngubah fungsi read_username_from_file di Listing 9-7 buat
balikin tipe error kustom namanya OurError yang kita definisikan sendiri. Kalau
kita juga mendefinisikan impl From<io::Error> for OurError buat ngonstruksi
sebuah instance dari OurError dari sebuah io::Error, maka pemanggilan
operator ? di dalem body read_username_from_file bakal manggil from dan
nge-convert tipe error-nya tanpa perlu nambahin kode lain lagi ke fungsinya.
Di konteks Listing 9-7, tanda ? di akhir pemanggilan File::open bakal
balikin nilai di dalem Ok ke variabel username_file. Kalau ada error yang
terjadi, operator ? bakal langsung keluar (return early) dari keseluruhan
fungsi dan ngasih nilai Err apa pun ke kode pemanggil. Hal yang sama juga
berlaku buat tanda ? di akhir pemanggilan read_to_string.
Operator ? ngilangin sangat banyak boilerplate code dan bikin implementasi
fungsi ini jadi lebih simpel. Kita bahkan bisa nyederhanain kode ini lebih
lanjut dengan nge-chaining (nyambungin) pemanggilan method langsung setelah ?,
kayak yang ditunjukin di Listing 9-8.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
}
?Kita udah mindahin pembuatan String baru di variabel username ke awal dari
fungsinya; bagian itu nggak berubah. Bukannya bikin variabel username_file,
kita nge-chaining pemanggilan read_to_string secara langsung ke hasil dari
File::open("hello.txt")?. Kita tetep punya tanda ? di akhir pemanggilan
read_to_string, dan kita tetep balikin nilai Ok yang nampung username
pas baik File::open maupun read_to_string berhasil, bukannya balikin
error-nya. Fungsionalitasnya tetep sama persis kayak di Listing 9-6 sama
Listing 9-7; ini cuma cara yang beda dan lebih ergonomis buat nulis kodenya.
Listing 9-9 nunjukin cara buat ngebikin ini jadi lebih singkat lagi pake
fs::read_to_string.
#![allow(unused)]
fn main() {
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
}
fs::read_to_string bukannya buka terus baca filenya secara manualBaca sebuah file jadi string itu operasi yang lumayan umum, jadi standard
library nyediain fungsi praktis fs::read_to_string yang buka filenya, bikin
String baru, baca isinya dari file, masukin isinya ke dalem String itu,
terus balikin nilai String-nya. Tentunya, kalau langsung pake
fs::read_to_string dari awal kita jadi nggak dapet kesempatan buat ngejelasin
semua penanganan error tadi, makanya kita bahas cara panjangnya dulu.
Di Mana Aja Operator ? Bisa Dipake
Operator ? cuma bisa dipake di fungsi-fungsi yang tipe kembaliannya (return
type) kompatibel (cocok) sama nilai di mana tanda ? itu dipake. Ini karena
operator ? didefinisikan buat ngelakuin early return (kembali lebih awal)
sebuah nilai keluar dari fungsi, sama kayak ekspresi match yang kita definisikan
di Listing 9-6. Di Listing 9-6, match-nya pake nilai Result, dan arm early
return-nya balikin nilai Err(e). Tipe kembalian dari fungsinya juga harus
berupa sebuah Result biar cocok sama return ini.
Di Listing 9-10, yuk kita liat error apa yang bakal kita dapet kalau kita nyoba
pake operator ? di dalem fungsi main yang punya tipe kembalian yang nggak
kompatibel sama tipe dari nilai di mana kita pake tanda ?.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
? di fungsi main yang balikin () nggak bakal bisa di-compile.Kode ini buka sebuah file, yang bisa aja gagal. Operator ? ngikutin nilai
Result yang dibalikin sama File::open, tapi fungsi main ini punya tipe
kembalian (), bukan Result. Pas kita nge-compile kode ini, kita bakal dapet
pesan error berikut:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
Error ini nunjukin kalau kita cuma dibolehin pake operator ? di fungsi yang
balikin Result, Option, atau tipe lain yang mengimplementasikan FromResidual.
Buat benerin error-nya, kita punya dua pilihan. Pilihan pertama adalah ngubah
tipe kembalian dari fungsinya biar kompatibel sama nilai yang lagi kita
kenain operator ? selama kita nggak punya batasan yang ngelarang hal itu.
Pilihan lainnya adalah pake sebuah match atau salah satu dari method di
Result<T, E> buat nanganin nilai Result<T, E>-nya dengan cara apa pun yang
paling pas.
Pesan error-nya juga nyebutin kalau ? bisa dipake bareng nilai Option<T>
juga. Sama kayak pas pake ? di dalem Result, kita cuma bisa pake ? di
dalem Option di sebuah fungsi yang juga balikin sebuah Option. Perilaku
dari operator ? pas dipanggil di dalem sebuah Option<T> itu mirip sama
perilakunya pas dipanggil di dalem sebuah Result<T, E>: kalau nilainya adalah
None, nilai None bakal dibalikin lebih awal dari fungsi di titik itu. Kalau
nilainya adalah Some, nilai di dalem Some-nya bakal jadi nilai hasil dari
ekspresi tersebut, dan fungsinya lanjut jalan. Listing 9-11 punya contoh
sebuah fungsi yang nyari karakter terakhir dari baris pertama di dalem sebuah
teks.
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
assert_eq!(
last_char_of_first_line("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
? di nilai Option<T>Fungsi ini balikin Option<char> karena mungkin aja ada karakter di sana, tapi
mungkin juga nggak ada. Kode ini ngambil argumen string slice text terus
manggil method lines pada string tersebut, yang bakal balikin sebuah iterator
yang ngelewatin baris-baris di string-nya. Karena fungsi ini pengen nge-cek
baris pertamanya, dia manggil next pada iterator-nya buat dapet nilai pertama
dari iterator tersebut. Kalau text isinya string kosong, pemanggilan
next ini bakal balikin None, dan di kasus itu kita pake ? buat berhenti
terus balikin None dari last_char_of_first_line. Kalau text bukan string
kosong, next bakal balikin nilai Some yang nampung string slice dari
baris pertama di dalem text.
Tanda ? ngekstrak string slice itu, dan kita bisa manggil chars pada
string slice tersebut buat dapet iterator dari karakter-karakternya. Kita
tertarik sama karakter terakhir di baris pertama ini, jadi kita manggil last
buat balikin item terakhir di iterator-nya. Ini adalah sebuah Option karena
mungkin aja baris pertamanya itu string kosong; misalnya, kalau text dimulai
pake baris kosong tapi punya karakter di baris lainnya, kayak di "\nhi". Tapi,
kalau emang ada karakter terakhir di baris pertama, dia bakal dibalikin di dalem
varian Some. Operator ? di tengah-tengah itu ngasih kita cara yang ringkas
buat mengekspresikan logika ini, ngebolehin kita mengimplementasikan fungsi ini
di dalem satu baris aja. Kalau kita nggak bisa pake operator ? di dalem Option,
kita harus mengimplementasikan logika ini pake lebih banyak pemanggilan method
atau pake ekspresi match.
Perhatiin ya kalau kita bisa pake operator ? di dalem Result di sebuah fungsi
yang balikin Result, dan kita bisa pake operator ? di dalem Option di
sebuah fungsi yang balikin Option, tapi kita nggak bisa nyampur aduk. Operator
? nggak bakal otomatis nge-convert sebuah Result jadi sebuah Option atau
sebaliknya; di kasus kayak gitu, kita bisa pake method kayak ok pada Result
atau method ok_or pada Option buat ngelakuin proses konversi-nya secara
eksplisit.
Sejauh ini, semua fungsi main yang udah kita pake itu balikin (). Fungsi
main itu spesial karena dia adalah entry point dan exit point dari program
executable, dan ada batasan soal tipe kembaliannya apa aja yang dibolehin
biar programnya bisa jalan sesuai ekspektasi.
Untungnya, main juga bisa balikin Result<(), E>. Listing 9-12 punya kode dari
Listing 9-10, tapi kita udah ubah tipe kembalian dari main jadi
Result<(), Box<dyn Error>> dan nambahin nilai kembalian Ok(()) di akhirnya.
Kode ini sekarang bakal bisa di-compile.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
main buat balikin Result<(), E> ngebolehin penggunaan operator ? pada nilai Result.Tipe Box<dyn Error> adalah sebuah trait object, yang bakal kita bahas di
“Menggunakan Trait Objects Yang Mengizinkan Nilai Dari Tipe Yang Beda-beda”
di Bab 18. Buat sekarang, kita bisa anggep Box<dyn Error> artinya “jenis error
apa pun.” Pake ? di dalem nilai Result di fungsi main yang punya tipe
error Box<dyn Error> itu dibolehin karena ini ngizinin nilai Err apa pun
buat dibalikin lebih awal. Walaupun body dari fungsi main ini cuma bakal pernah
balikin error bertipe std::io::Error, dengan nentuin Box<dyn Error>,
signature ini bakal tetep bener biarpun nanti ada lebih banyak kode yang
balikin error tipe lain yang ditambahin ke dalem body main.
Pas fungsi main balikin Result<(), E>, executable-nya bakal exit (keluar)
dengan nilai 0 kalau main balikin Ok(()) dan bakal exit dengan nilai selain
nol kalau main balikin nilai Err. Program executable yang ditulis dalam C
bakal balikin integer pas mereka exit: program yang berhasil bakal balikin
integer 0, dan program yang error bakal balikin integer selain 0. Rust juga
balikin integer dari program executable buat tetep kompatibel sama konvensi ini.
Fungsi main bisa balikin tipe apa pun yang mengimplementasikan trait
std::process::Termination, yang punya fungsi report yang
balikin sebuah ExitCode. Cek dokumentasi standard library buat info lebih
lanjut soal gimana cara mengimplementasikan trait Termination buat tipe kustom
kita sendiri.
Sekarang setelah kita bahas detail soal manggil panic! atau balikin Result,
yuk kita balik ke topik soal gimana cara mutusin mana yang pas buat dipake di
kasus yang mana.