A.40. Tipe Data ➜ Result
Chapter ini membahas tentang tipe data Result
, yaitu tipe data yang digunakan untuk menampung nilai yang isinya bisa berupa penanda operasi sukses (Ok
) dan data, atau error (Err
) beserta keterangan errornya.
Rust menyediakan panic-level error, tapi penggunaannya untuk menandai operasi error sangat tidak dianjurkan. Selain itu, Rust tidak mengenal konsep exception.
Tipe Result
ini adalah tipe yang paling umum dan direkomendasikan untuk digunakan dalam penanganan error.
Lebih detailnya mengenai penanganan error dibahas di chapter Error ➜ Recoverable Error & Error Handling
A.40.1. Konsep Result
Tipe data Result
adalah enum dengan isi 2 buah enum value:
Result::Ok<T>
(atauOk<T>
), digunakan untuk menandai bahwa data isinya adalah kabar baik (oke / mantab / jos / sukses).Result::Err<E>
(atauErr<E>
), digunakan untuk menandai bawah data berisi kabar buruk (error).
T
danE
merupakan parameter generic. Lebih jelasnya mengenai generic dibahas pada chapter Generics.
Tipe data Result
memiliki notasi penulisan Result<T, E>
yang mana T
digunakan pada enum value Ok<T>
dan E
digunakan enum value Err<E>
.
Cara penerapan tipe data ini bisa dilihat pada kode berikut:
#[derive(Debug)]
enum MathError {
DivisionByZero,
InfinityNumber,
OtherError,
}
fn main() {
let result1 = divider(10.0, 5.0);
println!("result: {:?}", result1);
let result2: Result<f64, MathError> = divider(10.0, 0.0);
println!("result: {:?}", result2);
}
fn divider(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
return Err(MathError::DivisionByZero);
}
let result = a / b;
return Ok(result);
}
Fungsi divider
di atas tugasnya adalah melakukan operasi aritmatika pembagian angka numerik f64
, parameter a
dibagi b
.
Pada fungsi tersebut terdapat pengecekan apabila nilai b
adalah 0
, maka yang dikembalikan adalah Err<E>
dengan E
berisi pesan error, selainnya maka hasil operasi pembagian dikembalikan dibungkus dalam enum value Ok<f64>
.
Fungsi divider
nilai baliknya bertipe Result<f64, MathError>
. Dari tipe data yang digunakan nantinya bisa diprediksi pasti akan ada 2 potensi value:
- Return value adalah enum value
Err<MathError>
, muncul ketika nilaib
adalah0
- Return value adalah nilai hasil numerik yang dibungkus oleh enum value
Ok<f64>
Output program di atas saat di-run:
A.40.2. Pattern matching pada tipe Result
Dalam penerapannya, ketika ada data bertipe Result
artinya data tersebut berpotensi untuk berisi nilai Err<E>
atau Ok<T>
, pasti antara 2 nilai tersebut.
Umumnya penggunaan tipe Result
selalu diikuti dengan pattern matching menggunakan keyword match
. Selain itu keyword if
sebenarnya juga bisa diterapkan pada pattern matching tipe data ini, namun kurang dianjurkan.
Mari kita praktikkan. Ubah isi fungsi main
dengan kode berikut:
let result = divider(10.0, 5.0);
match result {
Err(m) => println!("ERROR! {:?}", m),
Ok(r) => println!("result: {r:.2}"),
}
Bisa dilihat pada kode di atas mudahnya pengambilan nilai m
dari Err(m)
dan juga r
dari Ok(r)
. Penerapan match
untuk seleksi kondisi biasa disebut dengan pattern matching dan teknik ini sangat fleksibel dan advance.
Sebagai contoh, dengan penerapan match yang seperti ini kita bisa meng-handle 5 skenario seleksi kondisi:
let result = divider(10.0, 5.0);
match result {
Err(MathError::DivisionByZero) => println!("ERROR! unable to divide number by 0"),
Err(MathError::InfinityNumber) => println!("ERROR! result is infinity number (∞)"),
Err(_) => println!("ERROR! unknown error"),
Ok(2.0) => println!("the result is 2"),
Ok(x) => println!("result: {x:.2}"),
}
- Kondisi ke-1: jika nilai adalah
Err(MathError::DivisionByZero)
, maka munculkan pesanERROR! unable to divide number by 0
. - Kondisi ke-2: jika nilai adalah
Err(MathError::InfinityNumber)
, maka munculkan pesanERROR! result is infinity number (∞)
. - Kondisi ke-3: jika nilai adalah
Err
selain dariErr(MathError::DivisionByZero)
danErr(MathError::InfinityNumber)
, maka munculkan pesanERROR! unknown error
. - Kondisi ke-4: jika nilai adalah
Ok(2.0)
, maka munculkan pesanthe result is 2
. - Kondisi ke-5: jika nilai adalah
Ok
selain dariOk(2.0)
, maka munculkan pesanresult: {x:.2}
.
◉ Tips pattern matching
Silakan perhatikan kode yang sudah kita praktikkan berikut ini:
let result = divider(10.0, 5.0);
match result {
Err(m) => println!("ERROR! {:?}", m),
Ok(r) => println!("result: {r:.2}"),
}
Penerapan pattern matching seperti contoh di atas memiliki konsekuensi, yaitu variabel r
hanya bisa diakses pada block Ok(r)
saja.
Adakalanya kita butuh untuk mengeluarkan variabel r
ke luar block. Hal seperti ini mudah untuk dilakukan, dan ada beberapa cara yang bisa dipilih, namun menurut penulis yang paling elegan adalah cara berikut ini:
fn main() {
let result: f64 = match divider(10.0, 5.0) {
Err(m) => {
println!("ERROR! {:?}", m);
0.0
},
Ok(r) => r,
};
println!("result: {:?}", result);
}
Statement divider(10.0, 5.0)
mengembalikan data bertipe Result<f64, MathError>
. Data tersebut digunakan pada keyword match
seperti biasa. Namun pada contoh di atas ada yang berbeda, yaitu return value dari statement match
ditampung ke variabel (result
).
Isi dari pattern matching match
sendiri ada dua:
- Ketika block
Err(m)
match, error di-print kemudian nilai0.0
dijadikan return statementmatch
. - Ketika block
Ok
match, datar
dijadikan return value statementmatch
.
Dengan penerapan pattern matching seperti di atas, maka variabel result
akan selalu berisi data hasil operasi divider(10.0, 5.0)
. Dengan pengecualian ketika ada error, pesan errornya dimunculkan kemudian hasil operasi pembagian di-set sebagai 0.0
.
Lebih jelasnya mengenai pattern matching dibahas pada chapter Pattern Matching
A.40.3. Method tipe data Result
◉ Method is_ok
& unwrap
Isi dari enum value Ok<T>
bisa diakses tanpa menggunakan keyword match
dengan cara memanfaatkan method unwrap
milik Result<T, E>
. Sebelum mengakses method tersebut sangat dianjurkan untuk mengecek apakah data berisi Ok<T> atau tidak
, karena jika data adalah Err<E>
pengaksesan method unwrap
menghasilkan error.
Pengecekan nilai ok atau tidak bisa dilakukan menggunakan method is_ok
.
let result = divider(10.0, 5.0);
if result.is_ok() {
let number = result.unwrap();
println!("result: {}", number);
// result: 2
}
◉ Method as_ref
Method as_ref
digunakan untuk mengakses reference T
dan E
pada Result<T, E>
. Method ini sering kali dibutuhkan untuk menghindari terjadinya move semantics pada owner data bertipe Result<T, E>
.
Method as_ref
mengembalikan data dalam tipe Result<&T, &E>
. Jadi reference yang dipinjam bukan milik Result
-nya melainkan milik T
dan E
.
let result: Result<f64, MathError> = divider(10.0, 0.0);
let result_borrow: Result<&f64, &MathError> = result.as_ref();
Lebih jelasnya mengenai move semantics dibahas pada chapter Ownership
◉ Method is_err
& err
Method err
mengembalikan data dalam tipe Err<E>
. Pada pengaksesan method ini, pastikan untuk mengecek apakah Result
berisi data error atau ok dengan via method is_err
. Selain itu, wajib untuk menggunakan method as_ref
sebelum method err
agar ownership data Result
tidak berpindah (move semantics).
let result = divider(10.0, 0.0);
if result.is_err() {
let err = result.as_ref().err();
let message = err.unwrap();
println!("error: {:?}", message);
// error: DivisionByZero
}
◉ Method ok
Aturan yang sama juga berlaku pada pengaksesan method ok
yang mengembalikan data Ok<T>
. Method as_ref
harus diakses terlebih dahulu sebelum memanggil method ok
agar tidak terjadi move semantics.
let result = divider(10.0, 5.0);
if result.is_ok() {
let data = result.as_ref().ok();
let number = data.unwrap();
println!("result: {:?}", number);
// result: 2
}
◉ Method unwrap_or_default
Method unwrap_or_default
milik Result<T, E>
mengembalikan nilai T
ketika data berisi Ok<T>
, namun jika data berisi Err<E>
maka yang dikembalikan adalah default value dari tipe data T
.
let result = divider(10.0, 0.0);
let number = result.unwrap_or_default();
println!("result: {}", number);
// result: 0
◉ Method unwrap_or
Method unwrap_or
milik Result<T, E>
mengembalikan nilai T
ketika data berisi Ok<T>
, namun jika data ternyata isinya adalah Err<E>
maka yang dikembalikan adalah argument pemanggilan method tersebut.
let result = divider(10.0, 0.0);
let number = result.unwrap_or(0.0);
println!("result: {}", number);
// result: 0
◉ Method unwrap_or_else
Method ini mengembalikan nilai T
ketika data berisi Ok<T>
, namun jika data isinya adalah Err<E>
maka yang dikembalikan adalah hasil eksekusi closure yang disisipkan saat memanggil method unwrap_or_else
. Contoh pengaplikasiannya:
let result = divider(10.0, 0.0);
let number = result.unwrap_or_else(|_| 0.0);
println!("result: {}", number);
// result: 0
Closure harus dalam notasi FnOnce(E) -> T
yang mana T
pada konteks ini adalah f64
.
Lebih jelasnya mengenai closure dibahas pada chapter Closures.
A.40.4. Operator ?
pada tipe Result
Tipe data Result
bisa digunakan pada operator ?
. Penjelasannya ada di chapter terpisah di Operator ?.
A.40.5. Error handling
Tipe data Result<T, E>
banyak digunakan pada fungsi-fungsi yang disediakan Rust standard library, selain itu tipe tersebut juga akan sering kita gunakan dalam real life project.
Tipe ini dimanfaatkan untuk error handling di Rust. Lebih jelasnya mengenai topik tersebut dibahas pada chapter Error ➜ Recoverable Error & Error Handling.
A.40.6. Tipe Result<(), E>
Di atas kita telah mempelajari dan mempraktekan fungsi divider()
yang fungsi tersebut mengembalikan 2 informasi, yaitu:
T
berisi nilai hasil pembagianE
berisi error saat operasi pembagian
Bentuk lain penerapan tipe data Result
adalah dengan menggunakan notasi Result<T, E>
dengan T
diisi tipe data ()
. Tipe ini cukup sering digunakan pada fungsi yang memiliki potensi error tapi kita hanya butuh informasi errornya saja tanpa nilai balik lainnya.
Sebagai contoh, pada kode berikut dibuat fungsi baru bernama divide_and_print()
. Dalam fungsi tersebut, operasi pembagian dilakukan. Jika sukses, nilainya langsung di-print; jika error, nilai errornya dikembalikan. Dari sini terlihat bahwa kita tidak membutuhkan fungsi tersebut untuk mengembalikan informasi selain error.
fn divide_and_print(a: f64, b: f64) -> Result<(), MathError> {
let res = divider(a, b);
match res {
Err(m) => {
println!("ERROR! {:?}", m);
Err(m)
},
Ok(n) => {
println!("result: {}", n);
Ok(())
},
}
}
fn main() {
let result = divide_and_print(10.0, 1.0);
}
Pada pemanggilan fungsi Ok()
gunakan nilai ()
untuk memenuhi kriteria tipe data Result<(), MathError>
.
A.40.7. Tipe Result<T>
Notasi tipe data Result
bawaan Rust Standard Library adalah Result<T, E>
. Namun, pada pengembangan software yang melibatkan banyak sekali library/dependency, adakalanya pembaca akan menemui notasi tipe data Result<T>
. Notasi tersebut dibuat oleh pengembang library/dependency untuk memperingkas tipe Result<T, SomeErrorType>
. Jadi tidak usah bingung.
Catatan chapter 📑
◉ Source code praktik
github.com/novalagung/dasarpemrogramanrust-example/../result_type