Lewati ke konten

Result di Rust

Kalau Option mewakili nilai yang mungkin tidak ada, Result mewakili operasi yang mungkin gagal. Bedanya dengan nilai yang hilang, sebuah kegagalan biasanya datang bersama alasannya.

Result adalah enum lain dari standard library, kali ini dengan dua varian yang sama-sama membawa data:

enum Result<T, E> {
Ok(T), // berhasil, berisi sebuah nilai
Err(E), // gagal, berisi sebuah error
}

.parse(), yang sudah kamu temui di halaman Type Casting, adalah contoh nyata yang bagus: mengubah teks menjadi angka bisa gagal kalau teksnya ternyata bukan angka:

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

Output:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Coba sendiri

InvalidDigit adalah cara Rust bilang bahwa teksnya berisi karakter yang bukan digit.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Hasil parse: {number}"),
Err(e) => println!("Gagal parse: {e}"),
}
}
▶ Coba sendiri

Bentuknya sama dengan menangani Option memakai match. Bedanya, cabang Err memberimu nilai error sungguhan untuk diperiksa atau dilaporkan, bukan sekadar “tidak ada apa-apa”.

Section titled “Menulis fungsi sendiri yang mengembalikan Result”
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("tidak bisa membagi dengan nol"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Hasil: {result}"),
Err(e) => println!("Error: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Hasil: {result}"),
Err(e) => println!("Error: {e}"),
}
}

Output:

Hasil: 5
Error: tidak bisa membagi dengan nol
▶ Coba sendiri

Mengembalikan Result<f64, String> memaksa setiap pemanggil untuk menghadapi kemungkinan gagal. Tidak ada cara untuk tidak sengaja memperlakukan pembagian dengan nol sebagai angka biasa.

Merangkai beberapa operasi yang masing-masing bisa gagal akan menghasilkan blok match yang bersarang dalam. Operator ? meratakannya. Artinya: “kalau ini Err, langsung kembalikan error itu dari fungsi saat ini; kalau tidak, berikan nilai di dalam Ok.”

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // langsung kembali kalau parse gagal
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("ups"));
}

Output:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Coba sendiri

Tanpa ?, baris itu butuh match sendiri hanya untuk mengambil number atau keluar lebih awal dengan error-nya. ? melakukan persis hal itu dalam satu karakter. Operator ini hanya bisa dipakai di dalam fungsi yang juga mengembalikan Result (atau Option), karena dari situlah ia punya tempat untuk mengembalikan error-nya.

Rust juga punya panic!, yang langsung menghentikan program. Hal itu dibahas lebih dalam di halaman berikutnya. Sebagai pegangan: pakai Result untuk kegagalan yang wajar ingin ditangani oleh pemanggil (input yang salah, file yang tidak ada); simpan panic! untuk situasi yang menandakan ada bug sungguhan, di mana melanjutkan program justru lebih buruk daripada berhenti.

Begitu fungsimu bisa gagal dengan beberapa cara yang berbeda, error berupa String biasa tidak lagi cukup. Halaman Error kustom menunjukkan cara mendefinisikan tipe error-mu sendiri, dan anyhow dan thiserror menunjukkan crate yang dipakai sebagian besar proyek Rust untuk memangkas kode boilerplate.

Tulis fn parse_age(input: &str) -> Result<u32, String>. Fungsi ini harus mengembalikan Err dengan pesan yang jelas kalau teksnya bukan angka, atau kalau angkanya lebih dari 150. Uji dengan "34", "abc", dan "200". Petunjuk: lakukan match pada input.parse::<u32>(), dan di dalam cabang Ok pakai if untuk memeriksa besarnya.

Lihat solusi
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} terlalu tua untuk jadi umur sungguhan"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' bukan angka")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("Umur: {age}"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

Umur: 34
Error: 'abc' bukan angka
Error: 200 terlalu tua untuk jadi umur sungguhan
▶ Coba sendiri

Tulis fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> yang mem-parse kedua string dan mengembalikan jumlahnya. Pakai ?, bukan match. Cetak hasilnya untuk ("10", "20") dan ("10", "dua puluh"). Petunjuk: kamu butuh satu ? untuk setiap parse, dan baris terakhir membungkus hasil penjumlahannya dengan Ok(...).

Lihat solusi
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "dua puluh"));
}

Output:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ Coba sendiri
  • Result<T, E> mewakili operasi yang bisa berhasil (Ok) atau gagal (Err) dengan sebuah nilai error.
  • match menangani kedua kasus secara eksplisit, polanya sama seperti Option.
  • Operator ? otomatis mengembalikan Err lebih awal, jadi kamu tidak butuh match setelah setiap pemanggilan yang bisa gagal.
  • Utamakan Result daripada panic! untuk error yang wajar bisa dipulihkan oleh pemanggil.

Kuis singkat

1. Apa dua varian dari Result<T, E>?

2. Apa yang dilakukan operator ? di dalam fungsi yang mengembalikan Result?

3. Menurut halaman ini, kapan sebaiknya kamu memilih Result daripada panic!?

4. Di contoh divide pada halaman ini, apa yang dikembalikan divide(10.0, 0.0)?

5. Kenapa operator ? hanya bisa dipakai di dalam fungsi yang juga mengembalikan Result (atau Option)?

Skor: 0 / 5