本文 AI 產出,尚未審核

Rust 錯誤處理:使用 ? 運算子


簡介

在 Rust 中,錯誤處理被設計成 安全且表達力十足 的機制。與許多語言使用例外 (exception) 不同,Rust 透過 Result<T, E> 型別將錯誤資訊明確地傳遞給呼叫端。雖然 Result 本身已經相當好用,但在日常開發中,我們常會看到大量的 matchif let 包裝程式碼,讓函式的主流程被錯誤檢查的樣板淹沒。

? 運算子正是為了解決這個問題而誕生的:它讓 錯誤的傳遞變得像寫普通程式碼一樣簡潔,同時保留了 Rust 編譯器在編譯期提供的嚴格檢查。掌握 ? 的使用方式,能讓你的程式碼更易讀、更易維護,也更符合 Rust 的設計哲學。

以下本文將從概念說明、實作範例、常見陷阱與最佳實踐,直到實務應用場景,完整介紹 ? 運算子的使用方式,適合剛踏入 Rust 的新手以及已有基礎的中階開發者參考。


核心概念

1. Result<T, E>? 的基本關係

Result<T, E> 是 Rust 標準庫中用來表示可能失敗的運算。

  • Ok(T) 表示成功,攜帶一個值 T
  • Err(E) 表示失敗,攜帶錯誤資訊 E

? 運算子只能用在返回型別為 Result(或 Option)的函式內部。它的行為可概括為:

let v = expr?;
  1. 如果 expr 回傳 Ok(v),則 v 直接被解構,程式繼續往下執行。
  2. 如果 expr 回傳 Err(e),則 立即返回 Err(e) 給呼叫端,等同於 return Err(e);

重點? 只會「提早返回」錯誤,不會改變錯誤的類型或內容。

2. 必須符合的函式簽名

使用 ? 的函式必須回傳 Result<_, E>(或 Option<_>),且錯誤型別 E 必須實作 From 轉換,以便將子函式的錯誤自動轉型。最常見的情況是:

fn foo() -> Result<i32, std::io::Error> {
    // ...
}

如果子函式回傳 Result<i32, std::num::ParseIntError>,則需要 From<ParseIntError> for std::io::Error(或自行定義錯誤型別)才能直接使用 ?

3. ?Option 上的使用

Option<T> 也支援 ?,行為類似:

let v = opt?;
  • Some(v) → 繼續執行。
  • None → 立即返回 None

這讓在需要同時處理 ResultOption 時,語意保持一致。


程式碼範例

以下示範 5 個實用範例,從最簡單的檔案讀取到自訂錯誤型別的傳遞,幫助你快速上手 ?

範例 1:最簡單的檔案讀取

use std::fs::File;
use std::io::{self, Read};

fn read_to_string(path: &str) -> Result<String, io::Error> {
    // `File::open` 回傳 Result,若失敗直接返回 Err
    let mut file = File::open(path)?;
    let mut contents = String::new();
    // `read_to_string` 也是 Result,錯誤同樣會被傳遞
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

說明? 讓我們不需要寫兩層 match,程式流程看起來就像「順序執行」的普通程式。

範例 2:結合 OptionResult

use std::num::ParseIntError;

fn parse_first_line(path: &str) -> Result<i32, ParseIntError> {
    let content = std::fs::read_to_string(path).expect("檔案不存在");
    // 取得第一行,若沒有則回傳 None → 直接返回 Err
    let first_line = content.lines().next()?; // <-- `Option` 上的 ?
    // 解析成 i32,若解析失敗則回傳 Err
    let number: i32 = first_line.trim().parse()?;
    Ok(number)
}

重點lines().next()? 會在找不到任何行時回傳 None,此時函式會自動變成 Result<_, _>,因此必須確保函式的返回型別能接受 None → 這裡利用 Option::ok_or 轉成 Result,或直接在 ? 前使用 ok_or_else

範例 3:自訂錯誤型別與 From 轉換

use std::fmt;
use std::io;
use std::num::ParseIntError;

// 1. 定義自己的錯誤型別
#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(ParseIntError),
}

// 2. 為每個子錯誤實作 From,以支援 `?` 的自動轉換
impl From<io::Error> for MyError {
    fn from(err: io::Error) -> MyError {
        MyError::Io(err)
    }
}
impl From<ParseIntError> for MyError {
    fn from(err: ParseIntError) -> MyError {
        MyError::Parse(err)
    }
}

// 3. 為錯誤實作 Display(可選,但建議)
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),
        }
    }
}

// 4. 使用 `?`,錯誤會自動轉成 MyError
fn read_number(path: &str) -> Result<i32, MyError> {
    let txt = std::fs::read_to_string(path)?; // io::Error → MyError::Io
    let num: i32 = txt.trim().parse()?;       // ParseIntError → MyError::Parse
    Ok(num)
}

說明:只要為子錯誤實作 From? 就能自動把錯誤「升級」成自訂錯誤型別,讓錯誤資訊更具語意。

範例 4:在 async 函式中使用 ?

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

async fn async_read(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path).await?; // 注意 `.await?`
    let mut buf = String::new();
    file.read_to_string(&mut buf).await?;
    Ok(buf)
}

要點:在 async 環境下,? 必須寫在 .await? 之後,因為 await 先把 Future 轉成 Result,再由 ? 處理錯誤。

範例 5:使用 ? 搭配 map_err 自訂錯誤訊息

use std::fs;
use std::io;

fn read_config(path: &str) -> Result<String, String> {
    // `map_err` 把 io::Error 轉成字串,之後的 `?` 會回傳 String
    let content = fs::read_to_string(path).map_err(|e| format!("讀取失敗: {}", e))?;
    Ok(content)
}

說明:有時候不想建立完整的錯誤型別,只想直接把錯誤訊息轉成字串,map_err 搭配 ? 就是快速解法。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記在函式簽名加 Result ? 只能在返回 ResultOption 的函式內使用,否則編譯錯誤。 確認函式返回型別,例如 fn foo() -> Result<_, _>
錯誤型別不匹配 子函式的錯誤類型與父函式的錯誤型別不相容,? 無法自動轉換。 為子錯誤實作 From,或使用 map_err 手動轉換。
Option 上使用 ?,但返回 Result OptionNone 會被視為 Err(()),導致型別不符。 使用 ok_or / ok_or_elseOption 轉成 Result?
async 函式忘記 .await? 直接寫 future? 會編譯失敗,因為 Future 不是 Result 必須先 .await,再使用 ?future.await?
過度使用 ? 隱藏錯誤上下文 只傳遞原始錯誤,可能缺少呼叫層的資訊。 結合 anyhow::Context 或自訂 map_err 添加額外訊息。

最佳實踐

  1. 盡量在函式最外層使用 ?,讓錯誤傳遞保持線性,避免在中間層做過多 match
  2. 為自訂錯誤實作 std::error::Error,配合 thiserroranyhow,可以讓錯誤堆疊更易讀。
  3. 在公共 API 中保留錯誤類型,而在應用層使用 anyhow::Result 包裝,以減少型別爆炸。
  4. 使用 #[must_use] 標記返回 Result 的函式,提醒呼叫者不要忽略錯誤。
  5. 在測試中驗證錯誤路徑,確保 ? 真正會在失敗時提前返回。

實際應用場景

1. 命令列工具的參數解析與檔案 I/O

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = std::env::args().collect();
    let path = args.get(1).ok_or("缺少檔案路徑參數")?;
    let content = std::fs::read_to_string(path)?;
    println!("檔案內容:\n{}", content);
    Ok(())
}
  • ? 讓每一步的錯誤(缺少參數、檔案不存在、讀取失敗)都能直接回傳給 mainmain 再統一打印錯誤訊息。

2. 網路服務的非同步請求

use reqwest::Client;

async fn fetch_json(url: &str) -> Result<serde_json::Value, reqwest::Error> {
    let client = Client::new();
    let resp = client.get(url).send().await?.json().await?;
    Ok(resp)
}
  • ? 在非同步流程中保持直線化,讓錯誤傳遞不會因 Future 的層層嵌套而變得難以追蹤。

3. 複雜資料轉換管線

fn process_data(csv_path: &str) -> Result<Vec<MyStruct>, Box<dyn std::error::Error>> {
    let txt = std::fs::read_to_string(csv_path)?;
    let mut records = Vec::new();
    for line in txt.lines() {
        let fields: Vec<&str> = line.split(',').collect();
        let id: u32 = fields.get(0).ok_or("缺少 ID")?.trim().parse()?;
        let name = fields.get(1).ok_or("缺少名稱")?.trim().to_string();
        records.push(MyStruct { id, name });
    }
    Ok(records)
}
  • 透過 ? 把 CSV 解析、欄位缺失、字串轉整數等錯誤全部統一向上層傳遞,程式碼保持簡潔且易於維護。

總結

? 運算子是 Rust 錯誤處理的核心利器,它把 錯誤的傳遞 變成 像寫普通程式碼一樣自然 的流程。只要遵守以下三個原則,就能在大多數情況下安全且高效地使用 ?

  1. 函式返回 Result(或 Option,且錯誤型別支援 From 轉換。
  2. 在需要的地方使用 map_errok_or 等輔助方法,確保錯誤上下文完整。
  3. 配合自訂錯誤型別或 anyhow/thiserror 等庫,讓錯誤資訊在跨層傳遞時保持可讀性。

掌握 ? 後,你會發現原本充斥 match 的錯誤檢查瞬間變得乾淨、直觀,讓程式碼更聚焦於「業務邏輯」本身。無論是同步的檔案 I/O、非同步的網路請求,或是資料轉換的管線作業,? 都能提供一致且安全的錯誤處理方式。

現在就把這些概念套用到自己的專案中,讓 Rust 的錯誤處理真正發揮「安全、表達力、易維護」的優勢吧!