跳至主要內容

Rust 錯誤處理模式:Result、Option 與 ? 運算子

Rust 錯誤處理模式:Result、Option 與 ? 運算子

Rust 的錯誤處理是其最具特色的設計之一。沒有例外(Exception),沒有 null,取而代之的是 Result<T, E>Option<T> 這兩個型別,強迫你在編譯時就處理所有可能的錯誤情況。一旦習慣了這套模式,你會發現它讓程式的可靠性大幅提升。

Option:處理可能不存在的值

Option<T> 替代了其他語言中的 null

enum Option<T> {
    Some(T),
    None,
}
fn find_user(id: u32) -> Option<String> {
    let users = vec![
        (1, "Alice".to_string()),
        (2, "Bob".to_string()),
    ];

    users.into_iter()
        .find(|(uid, _)| *uid == id)
        .map(|(_, name)| name)
}

fn main() {
    match find_user(1) {
        Some(name) => println!("找到使用者:{}", name),
        None => println!("使用者不存在"),
    }

    // if let 語法(更簡潔)
    if let Some(name) = find_user(2) {
        println!("使用者:{}", name);
    }

    // unwrap_or:提供預設值
    let name = find_user(999).unwrap_or("匿名".to_string());
    println!("{}", name);  // 匿名

    // unwrap_or_else:惰性計算預設值
    let name = find_user(999).unwrap_or_else(|| {
        "Guest_".to_string() + &generate_id().to_string()
    });
}

Result:處理可能失敗的操作

enum Result<T, E> {
    Ok(T),
    Err(E),
}
use std::fs;
use std::num::ParseIntError;

fn read_number_from_file(path: &str) -> Result<i32, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| format!("讀取檔案失敗:{}", e))?;

    let number = content.trim().parse::<i32>()
        .map_err(|e| format!("解析數字失敗:{}", e))?;

    Ok(number)
}

fn main() {
    match read_number_from_file("number.txt") {
        Ok(n) => println!("讀取到數字:{}", n),
        Err(e) => eprintln!("錯誤:{}", e),
    }
}

? 運算子:優雅的錯誤傳播

? 是 Rust 最方便的語法糖,它等同於:

// 沒有 ? 的寫法
let result = some_function();
let value = match result {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
};

// 有 ? 的寫法
let value = some_function()?;

實際範例:

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

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
}

fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;           // io::Error
    let value: serde_json::Value = serde_json::from_str(&content)?;  // serde_json::Error

    let host = value["host"]
        .as_str()
        .ok_or("缺少 host 欄位")?
        .to_string();

    let port = value["port"]
        .as_u64()
        .ok_or("缺少 port 欄位")? as u16;

    Ok(Config { host, port })
}

自訂錯誤類型

使用 thiserror 套件(推薦)

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("資料庫錯誤:{0}")]
    Database(#[from] sqlx::Error),

    #[error("驗證失敗:{field} - {message}")]
    Validation {
        field: String,
        message: String,
    },

    #[error("找不到資源:{0}")]
    NotFound(String),

    #[error("未授權")]
    Unauthorized,
}

async fn get_user(db: &Pool, id: i32) -> Result<User, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(db)
        .await?;  // sqlx::Error 自動轉換為 AppError::Database

    user.ok_or_else(|| AppError::NotFound(format!("使用者 {} 不存在", id)))
}

手動實作 Error trait

use std::fmt;

#[derive(Debug)]
struct CustomError {
    message: String,
    code: u32,
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}] {}", self.code, self.message)
    }
}

impl std::error::Error for CustomError {}

組合 Option 和 Result

// Option 轉 Result
fn parse_age(s: Option<&str>) -> Result<u8, String> {
    s.ok_or("缺少年齡欄位".to_string())?
        .parse::<u8>()
        .map_err(|e| format!("年齡格式錯誤:{}", e))
}

// 在 Iterator 中處理錯誤
fn parse_numbers(strings: Vec<&str>) -> Result<Vec<i32>, String> {
    strings
        .into_iter()
        .map(|s| s.parse::<i32>().map_err(|e| format!("'{}' 不是有效數字: {}", s, e)))
        .collect()  // Vec<Result<i32, String>> → Result<Vec<i32>, String>
}

fn main() {
    let numbers = parse_numbers(vec!["1", "2", "3"]);
    println!("{:?}", numbers);  // Ok([1, 2, 3])

    let error = parse_numbers(vec!["1", "abc", "3"]);
    println!("{:?}", error);    // Err("'abc' 不是有效數字: ...")
}

在 Web 框架中的錯誤處理(Axum)

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

// 讓自訂錯誤可以直接回傳 HTTP 回應
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "未授權".to_string()),
            AppError::Validation { message, .. } => (StatusCode::BAD_REQUEST, message.clone()),
            AppError::Database(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "資料庫錯誤".to_string(),
            ),
        };

        (status, Json(json!({ "error": message }))).into_response()
    }
}

// Handler 直接回傳 Result
async fn get_user_handler(
    Path(id): Path<i32>,
    State(db): State<Pool>,
) -> Result<Json<User>, AppError> {
    let user = get_user(&db, id).await?;
    Ok(Json(user))
}

總結

Rust 的錯誤處理強迫你在編譯時就思考所有可能的失敗情況,雖然一開始會覺得繁瑣,但換來的是極高的程式可靠性。? 運算子大幅簡化了錯誤傳播的語法,搭配 thiserror 套件定義清晰的錯誤類型,可以寫出既安全又易讀的錯誤處理程式碼。

分享這篇文章