Fungsi di dunia nyata jarang berdiri sendiri: cek_pendaftaran memanggil parse_usia, yang memanggil parse; kegagalan di lapisan terdalam harus menempuh perjalanan naik sampai ke main sebelum ada yang berwenang memutuskan. Penyerahan error dari fungsi dalam ke fungsi pemanggilnya disebut propagation, dan pola match-nya sudah kita kenal: bila Err, kembalikan error itu segera. Pola tersebut benar, tetapi boros — lima baris upacara untuk satu baris niat, berulang di setiap lapisan. Rust mengganti seluruh ritual itu dengan satu karakter: operator tanya ?. Pada chapter ini kita akan membandingkan kedua bentuk, membongkar cara kerja ? di balik layar, lalu memakainya untuk membangun rantai pemanggilan yang error-nya mengalir naik dengan tenang.
Masalah Propagasi Error #
Kita mulai dari bentuk manual yang jujur. Fungsi berikut membaca usia dari teks; satu versi menuliskan propagasi dengan match, satu versi lagi memakai ?:
fn parse_usia_manual(input: &str) -> Result<u32, std::num::ParseIntError> {
let hasil_parse = input.parse::<u32>();
let usia = match hasil_parse {
Ok(nilai) => nilai,
Err(error) => return Err(error),
};
Ok(usia)
}
fn parse_usia(input: &str) -> Result<u32, std::num::ParseIntError> {
let usia = input.parse::<u32>()?;
Ok(usia)
}
fn main() {
println!("Manual : {:?}", parse_usia_manual("34"));
println!("Ringkas: {:?}", parse_usia("34"));
println!("Ringkas: {:?}", parse_usia("abc"));
}Output yang Dihasilkan:
Manual : Ok(34)
Ringkas: Ok(34)
Ringkas: Err(ParseIntError { kind: InvalidDigit })Mari kita bedah kode di atas:
- Baris 1-9 (
fn parse_usia_manual): versi manual membungkus hasilparsedalammatch; cabangOkmengeluarkan nilainya, cabangErrmemicureturn Err(error)— error dikirim pulang ke pemanggil saat itu juga, melewati sisa fungsi. - Baris 10-13 (
fn parse_usia): versi?mengerjakan hal yang persis sama dalam satu karakter. Ekspresiinput.parse::<u32>()?berarti: “parse teks ini; kalau sukses, berikan nilainya keusia; kalau gagal, hentikan fungsi ini dan kirim error-nya ke pemanggil”. - Baris 16-18 (pemanggilan): hasil kedua fungsi identik untuk masukan yang sama —
"34"menghasilkanOk(34),"abc"menghasilkanErr— membuktikan?setara penuh denganmatchversi manual.
Yang hilang dari versi ringkas hanya upacara; keputusan tetap sama. Fungsi tetap berjanji Result pada tanda tangan, error tetap dikirim ke pemanggil, dan pemanggil tetap memutuskan langkah berikutnya.
Cara Kerja Operator ? di Balik Layar #
Secara mesin, ekspresi? diekspansi menjadi bentuk yang hampir sama dengan match manual kita — kurang lebih seperti ini:
let nilai = match ekspresi {
Ok(nilai) => nilai,
Err(error) => return Err(error),
};Tiga perilaku mengikuti bentuk tersebut. Pertama, ? hanya berlaku di fungsi yang tipe kembaliannya Result — cabang return Err(...) tidak akan masuk akal pada fungsi yang berjanji mengembalikan u32 polos; kompilator menolak pemakaian seperti itu dengan error tipe. Kedua, saat Err ditemui, sisa fungsi tidak dieksekusi sama sekali: baris-baris setelah ? melewati jalur kegagalan, sama seperti return. Ketiga, saat Ok ditemui, isi nilainya keluar dari ekspresi dan tipe usia menjadi u32 — bungkus Result sudah terbuka secara otomatis.
Propagasi Berlapis di Rantai Pemanggilan #
Kekuatan ? paling terasa ketika kegagalan harus menembus beberapa lapis fungsi. Program berikut memvalidasi pendaftaran kursus: cek_pendaftaran memanggil parse_usia, dan main memanggil cek_pendaftaran:
fn parse_usia(input: &str) -> Result<u32, std::num::ParseIntError> {
let usia = input.parse::<u32>()?;
Ok(usia)
}
fn cek_pendaftaran(input: &str) -> Result<String, std::num::ParseIntError> {
let usia = parse_usia(input)?;
if usia >= 17 {
Ok(String::from("Pendaftaran diterima"))
} else {
Ok(String::from("Pendaftaran ditolak: usia di bawah 17 tahun"))
}
}
fn main() {
for input in ["21", "15", "abc"] {
match cek_pendaftaran(input) {
Ok(pesan) => println!("Input '{}' -> {}", input, pesan),
Err(error) => println!("Input '{}' -> gagal diproses: {}", input, error),
}
}
}Output yang Dihasilkan:
Input '21' -> Pendaftaran diterima
Input '15' -> Pendaftaran ditolak: usia di bawah 17 tahun
Input 'abc' -> gagal diproses: invalid digit found in stringMari kita telaah kode di atas:
- Baris 2 (
let usia = input.parse::<u32>()?;): lapisan pertama?. Teks"abc"gagal di sini dan langsung dikirim keluar dariparse_usiasebagaiErr(ParseIntError). - Baris 7 (
let usia = parse_usia(input)?;): lapisan kedua?. Pemanggilanparse_usiamenghasilkanErr, jadi?menghentikancek_pendaftaran— blokif usia >= 17di bawahnya tidak pernah dieksekusi untuk teks"abc"— dan error yang sama diteruskan kemain. - Baris 9-13 (
if usia >= 17 { ... }): logika domain yang bersih tanpa satu pun cabang error; seluruh kegagalan sudah selesai di baris bertanda?. Jalur sukses menghasilkanOk(String)berisi pesan keputusan. - Baris 17-21 (
match cek_pendaftaran(input)):mainadalah pemanggil terakhir yang akhirnya mengambil keputusan: cabangOkmencetak pesan hasil, cabangErrmencetak laporan kegagalan. Untuk masukan"abc", pesan yang tercetak berasal dariParseIntErroryang menempuh dua lapis fungsi tanpa pernah disentuh.
Perjalanan error untuk teks "abc" dapat kita petakan:
Diagram di atas menunjukkan inti propagation: error dari lapisan terdalam berpindah tangan naik, lapis demi lapis, sampai tiba di titik yang berwenang memutuskan — pada contoh kita, main yang memilih menampilkan pesan. ? tidak memilih jalur kegagalan; ? hanya memastikan perjalanan naiknya sependek mungkin.
Ringkasan #
Operator tanya ? memampatkan pola propagasi match-return Err menjadi satu karakter: Ok terbuka dan nilainya dipakai, Err menghentikan fungsi saat itu juga dan dikirim ke pemanggil. Ekspansi mekanismenya identik dengan match manual, sehingga tidak ada perilaku tersembunyi. Pada rantai berlapis, error dari lapisan terdalam melompat keluar melewati semua lapisan ? hingga tiba di pemanggil pengambil keputusan. Satu titik yang belum kita sentuh: fungsi main sendiri selama ini selalu memakai match di ujung rantai — padahal main juga bisa mengembalikan Result dan memakai ?. Chapter berikutnya membuka kemungkinan tersebut dengan Box<dyn Error>.