Rust 錯誤處理:Result 型別與 unwrap、expect
簡介
在系統程式語言裡,錯誤處理往往是程式安全與穩定性的關鍵。Rust 採用 Result 型別作為顯式的錯誤傳遞機制,讓編譯器在編譯階段就能提醒開發者必須處理可能失敗的情況。相較於傳統的例外拋出(exception)機制,Result 以 值 的形式呈現成功或失敗,讓錯誤處理變成程式流程的一部份。
本篇文章將深入說明 Result 的結構、常見的 unwrap 與 expect 方法,並透過實作範例展示它們在 開發、除錯與正式環境 中的適當使用時機。即使你是剛接觸 Rust 的新手,也能在閱讀完後,快速在自己的專案裡寫出安全且易於維護的錯誤處理程式碼。
核心概念
1. Result<T, E> 的基本型別
Result 是一個 枚舉(enum),定義在標準函式庫 std::result 中:
enum Result<T, E> {
Ok(T), // 操作成功,攜帶成功值
Err(E), // 操作失敗,攜帶錯誤資訊
}
T代表 成功 時回傳的資料型別。E代表 失敗 時回傳的錯誤型別,通常實作了std::error::Errortrait。
使用 Result 的好處在於 編譯期保證:只要函式回傳 Result,呼叫端必須顯式處理 Ok 或 Err,否則程式碼無法編譯通過。
2. 常見的錯誤型別:std::io::Error、自訂錯誤
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // `?` 會自動把 Err 向上傳遞
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
在上例中,File::open 與 read_to_string 都回傳 Result<_, io::Error>,透過 ? 操作子即可自動將錯誤向上層傳遞,保持程式碼簡潔。
3. unwrap 與 expect
| 方法 | 行為 | 何時使用 |
|---|---|---|
unwrap() |
若為 Ok(v) 回傳 v,若為 Err(e) panic! 並顯示預設訊息 |
快速測試、原型開發,不建議在正式產品中使用 |
expect(msg) |
與 unwrap 相同,但在 panic 時顯示自訂訊息 msg |
需要在失敗時提供更具體的除錯資訊 |
let number: i32 = "42".parse().unwrap(); // 成功,回傳 42
let number: i32 = "abc".parse().expect("解析失敗"); // 失敗,panic 並顯示「解析失敗」
⚠️ 注意:
unwrap與expect會導致程式在執行時直接中止,若錯誤來源不在開發階段,就會影響使用者體驗。
4. match 與 if let 處理 Result
match read_file("data.txt") {
Ok(text) => println!("檔案內容: {}", text),
Err(e) => eprintln!("讀檔失敗: {}", e),
}
if let Ok(text) = read_file("data.txt") {
println!("檔案內容: {}", text);
} else {
eprintln!("讀檔失敗");
}
5. ? 操作子:錯誤傳遞的糖衣
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse()?; // 若 parse 失敗,直接回傳 Err
Ok(n)
}
? 只適用於回傳型別為 Result<_, E>(或 Option)的函式,讓錯誤「向上冒泡」而不需要手動寫 match。
程式碼範例
範例 1:簡易的檔案讀取與 unwrap
use std::fs;
fn main() {
// 直接使用 unwrap,若檔案不存在會 panic
let content = fs::read_to_string("hello.txt").unwrap();
println!("檔案內容: {}", content);
}
說明:此程式在開發階段快速驗證檔案是否正確,但在正式環境應改用 match 或 expect。
範例 2:使用 expect 提供自訂錯誤訊息
use std::fs;
fn main() {
let content = fs::read_to_string("config.toml")
.expect("無法讀取設定檔,請確認 config.toml 是否存在於執行目錄");
println!("設定檔內容: {}", content);
}
說明:當程式失敗時,錯誤訊息會直接告訴使用者缺少哪個檔案,提升除錯效率。
範例 3:Result 搭配 match 處理多種錯誤
use std::fs::File;
use std::io::{self, Read};
fn read_first_line(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf.lines().next().unwrap_or("").to_string())
}
fn main() {
match read_first_line("notes.txt") {
Ok(line) => println!("第一行: {}", line),
Err(e) => eprintln!("讀取失敗: {}", e),
}
}
說明:此範例展示 ?、match 與 unwrap_or 的結合,讓錯誤處理保持一致且易於閱讀。
範例 4:自訂錯誤型別與 From 轉換
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(ParseIntError),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, "IO 錯誤: {}", e),
MyError::Parse(e) => write!(f, "解析錯誤: {}", e),
}
}
}
// 讓標準錯誤自動轉成 MyError
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}
impl From<ParseIntError> for MyError {
fn from(e: ParseIntError) -> Self { MyError::Parse(e) }
}
fn read_number(path: &str) -> Result<i32, MyError> {
let txt = std::fs::read_to_string(path)?; // 會自動轉成 MyError::Io
let num: i32 = txt.trim().parse()?; // 會自動轉成 MyError::Parse
Ok(num)
}
說明:透過 From 實作,? 能在不同錯誤來源之間自動轉換,讓函式的回傳型別保持一致。
範例 5:在測試中使用 unwrap
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_number() {
// 測試環境允許直接 unwrap,因為失敗代表測試失敗
let n = "123".parse::<i32>().unwrap();
assert_eq!(n, 123);
}
}
說明:在單元測試裡,unwrap 常被用來讓測試失敗時立即中斷,方便定位錯誤。
常見陷阱與最佳實踐
過度使用
unwrap/expect- 在 生產環境 中直接 panic 會導致服務中斷。除非真的確定錯誤不會發生(例如硬編碼的檔案路徑),否則應改用
match、if let或?。
- 在 生產環境 中直接 panic 會導致服務中斷。除非真的確定錯誤不會發生(例如硬編碼的檔案路徑),否則應改用
忽略錯誤資訊
- 使用
unwrap時錯誤訊息僅顯示panic!,缺乏上下文。expect能提供自訂訊息,建議在開發階段先改為expect,再逐步改寫為更完整的錯誤處理。
- 使用
錯誤型別不一致
- 多個函式回傳不同的錯誤型別時,使用
?會產生型別不匹配的編譯錯誤。透過 自訂錯誤列舉 並實作From轉換,可統一錯誤型別。
- 多個函式回傳不同的錯誤型別時,使用
在迴圈內使用
?直接返回- 若迴圈內的每一次迭代都可能失敗,直接使用
?會在第一次失敗時提前返回。若需要收集所有錯誤,應改用Result<Vec<_>, _>或自行累積錯誤。
- 若迴圈內的每一次迭代都可能失敗,直接使用
忘記
use std::error::Error- 若要把自訂錯誤當作
Box<dyn Error>使用,需要為錯誤實作std::error::Errortrait,否則會出現型別不匹配。
- 若要把自訂錯誤當作
最佳實踐清單
- ✅ 盡量使用
?讓錯誤自動向上傳遞,保持函式簡潔。 - ✅ 在公共 API(函式、庫)返回
Result,讓呼叫者自行決定如何處理。 - ✅ 在測試或原型 中暫時使用
unwrap,但在完成後務必替換為正式的錯誤處理。 - ✅ 提供有意義的錯誤訊息(使用
expect或自訂錯誤),有助於除錯與日誌分析。 - ✅ 使用
thiserror、anyhow等社群套件簡化錯誤型別的定義與轉換。
實際應用場景
| 場景 | 建議的錯誤處理方式 |
|---|---|
| CLI 工具:解析使用者參數、讀寫檔案 | 使用 Result + ?,最外層 main 使用 std::process::exit 回傳錯誤代碼,並在 eprintln! 中印出 expect 訊息。 |
Web 伺服器(如 actix-web、warp) |
每個 handler 回傳 Result<impl Responder, MyError>,框架會自動轉換為 HTTP 錯誤回應。 |
| 嵌入式開發:硬體 I/O 失敗 | 以 Result<(), MyError> 包裝,避免 panic 造成系統重啟,改以錯誤碼回報。 |
| 資料庫存取:SQL 執行失敗 | 使用 ? 搭配自訂錯誤,並在上層統一記錄錯誤與重試策略。 |
| 單元測試:驗證函式行為 | 直接使用 unwrap 或 expect,測試失敗即為錯誤訊息。 |
總結
Result<T, E> 是 Rust 安全、顯式 的錯誤傳遞機制,讓開發者在編譯階段就必須面對可能的失敗。unwrap 與 expect 提供了快速檢查的便利,但不應成為正式程式碼的主要錯誤處理手段。透過 match、if let、? 以及自訂錯誤列舉,我們可以在保持程式可讀性的同時,提供完整且具意義的錯誤資訊。
在實務開發中,遵循以下幾點即可寫出更健全的 Rust 程式:
- 盡量返回
Result,讓呼叫者自行決定處理方式。 - 使用
?讓錯誤自動向上冒泡,減少樣板程式碼。 - 在需要快速失敗的情境(測試、原型)才使用
unwrap/expect,並在正式版中替換。 - 自訂錯誤型別 並實作
From,統一錯誤介面,提升可維護性。 - 提供清晰的錯誤訊息,讓使用者與開發者都能快速定位問題。
掌握了 Result、unwrap、expect 的正確使用方式後,你將能在 Rust 專案中建立 可靠、可預測 的錯誤處理流程,讓程式在面對不確定的外部環境時仍能保持穩定運行。祝你寫程式快樂,錯誤處理無慮!