Result<T, E> 與以值傳遞的錯誤處理(Result<T, E>)
一個函式要怎麼告訴呼叫者出了狀況?在 C 裡常見的答案都很笨拙:回傳一個特殊的哨兵值(像 -1 或 NULL),呼叫者必須記得去檢查;再設定一個獨立的全域變數 errno,裡頭存著真正的錯誤碼。問題眾所周知——很容易忘記檢查、真正的錯誤住在一個下一次呼叫就可能覆寫的全域裡、而且函式的型別對「可能發生哪些錯誤」隻字未提。Rust 用一個從函式回傳的普通值取代了這一切:Result<T, E>。
Result<T, E> 是一個剛好有兩種形狀的型別,很像 Option。Ok(value) 表示成功了、這是型別為 T 的結果;Err(error) 表示失敗了、這是型別為 E、描述出了什麼錯的錯誤。錯誤就在回傳值本身裡頭傳回給呼叫者——沒有隱藏的全域、沒有會看錯的哨兵。因為一個可能失敗的函式回傳型別是 Result,編譯器讓每個呼叫者都得面對兩種結果:你不可能在還沒承認 Err 情形之前就拿到 Ok 的值,通常用 match。這就是以值傳遞的錯誤處理:錯誤是你收到、檢視、並往下傳的普通值,而不是塞在某個角落的旗標。
在每一層都手動把錯誤往上傳會很煩,所以 Rust 加了一個標點:? 運算子。寫 let f = File::open(path)?; 的意思是:若這個 Result 是 Ok,就把它拆出來、帶著值繼續;若它是 Err,就立刻從外層函式回傳同一個 Err。所以 ? 用一個字元表達了「傳播錯誤、或繼續往下」,它讓常見路徑——試試看,並把失敗冒泡給真正能處理的人——讀起來幾乎和忽略錯誤的程式碼一樣乾淨,卻仍然受檢查。誠實的說法:Result 不會讓錯誤消失,也不會替你處理它;它讓「忽略一個錯誤」變成一個看得見的選擇,而不是像 C 那個無聲未檢查的回傳碼那樣的一次輕易意外。你仍然得決定要怎麼做——復原、重試、回報、或放棄。
use std::num::ParseIntError; fn double(s: &str) -> Result<i32, ParseIntError> { let n = s.parse::<i32>()?; // Ok -> 拆出來;Err -> 立刻回傳它 Ok(n * 2) } match double("21") { Ok(v) => println!("得到 {v}"), // 得到 42 Err(e) => println!("錯誤輸入:{e}"), }
錯誤搭著回傳值本身回到呼叫端;? 替你拆出 Ok 或回傳 Err。
Result 是把 C 的「回傳碼加 errno」想法做成一個編譯器逼你檢視的型別。? 運算子只是把錯誤往上傳播;它不處理錯誤,所以某處的某個呼叫者仍得決定失敗代表什麼。