Result 和 Option:Rust 风格的错误处理

1. 概述

Rust 没有像 Java 或 C# 那样的异常。相反,Rust 使用两种数据类型 Option<T> 和 Result<T, E> 来表示“可能没有值”和“可能失败”。这篇文章将帮你理解如何正确使用这两种类型,如何用 ? 操作符简洁地传播错误,以及如何用 thiserror 定义自定义错误。读完后,你将能够编写清晰的 Rust 错误处理代码,无需 try/catch。

2. 为什么不用异常?

在 Java 中,一个方法可以随时抛出 RuntimeException,而方法签名中完全没有声明。你必须阅读 Javadoc 或代码才能知道它可能抛出什么。

Rust 强制你在返回类型中直接声明失败的可能性:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("除以 0".to_string());
    }
    Ok(a / b)
}

看到签名 Result<i32, String> 就知道这个函数可能会失败。编译器强制你处理 Ok 和 Err 两个分支,不能像 Java 中忘记 catch 那样悄悄跳过错误。

注意: Rust 仍然有 panic! 用于不可恢复的错误(编程 bug、不变量违反)。Result 用于可预测和可处理的错误。

3. Option:可能缺失的值

Option<T> 替代 null。它有两个变体:Some(T) 和 None。

fn find_customer(id: u32) -> Option<String> {
    let customers = vec![(1, "Alice"), (2, "Bob")];
    customers.iter()
        .find(|(cid, _)| *cid == id)
        .map(|(_, name)| name.to_string())
}

fn main() {
    match find_customer(2) {
        Some(name) => println!("找到了:{name}"),
        None => println!("没有这个客户"),
    }
}

find 返回 Option<&(u32, &str)>,map 在 Some 内部转换值,如果是 None 则跳过。结果:

找到了:Bob

3.1. 常用方法

let maybe_age: Option<u32> = Some(25);

let age = maybe_age.unwrap_or(18);          // 25,或 18 若为 None
let doubled = maybe_age.map(|a| a * 2);     // Some(50)
let result: Result<u32, &str> = maybe_age.ok_or("缺少年龄"); // Ok(25)
  • unwrap_or(default):取值或返回默认值,永不 panic。
  • map(f):如果有值就在内部转换,没有则保留 None。
  • ok_or(err):将 Option<T> 转换为 Result<T, E>——当你后面需要用 ? 操作符时很有用。

注意: 避免在生产代码中直接调用 .unwrap()(在 Option/Result 上)。一旦遇到 None/Err 就会 panic,相当于抛出不可控的 NullPointerException。只在测试中或你 100% 确定值存在时使用。

4. Result<T, E>:成功或失败

Result<T, E> 有两个变体:Ok(T) 表示成功,Err(E) 表示失败。

use std::num::ParseIntError;

fn parse_quantity(input: &str) -> Result<u32, ParseIntError> {
    input.trim().parse::<u32>()
}

fn main() {
    match parse_quantity("  42 ") {
        Ok(qty) => println!("数量:{qty}"),
        Err(e) => println!("解析错误:{e}"),
    }
}

parse::<u32>() 返回 Result<u32, ParseIntError>。match 强制你处理两个分支——如果缺少任何分支,编译器会拒绝编译。

数量:42

4.1. 用 ? 操作符传播错误

到处写 match 很冗长。? 操作符在遇到 Err 时立即传播错误,类似自动 throw:

use std::num::ParseIntError;

struct Order {
    quantity: u32,
    price: u32,
}

fn parse_order(qty_str: &str, price_str: &str) -> Result<Order, ParseIntError> {
    let quantity = qty_str.trim().parse::<u32>()?;
    let price = price_str.trim().parse::<u32>()?;
    Ok(Order { quantity, price })
}

fn main() {
    match parse_order("3", "15000") {
        Ok(order) => println!("总金额:{}", order.quantity * order.price),
        Err(e) => println!("数据无效:{e}"),
    }
}

行 qty_str.trim().parse::<u32>()? 的意思是:如果 parse 返回 Err,函数 parse_order 立即 返回那个 Err 给调用者,无需手动编写 match。如果是 Ok,内部值被“解包”并赋给 quantity。

总金额:45000

注意: ? 操作符只有在当前函数和被调用函数的错误类型兼容时才能使用(或有 From 实现进行转换)。否则编译器会在构建时报告类型不匹配错误。

5. 结合 Option 和 Result

这两种类型经常交织使用。例如,在库存中查找产品,然后检查库存是否足够:

struct Product {
    name: String,
    stock: u32,
}

fn find_product(name: &str, products: &[Product]) -> Option<&Product> {
    products.iter().find(|p| p.name == name)
}

fn check_stock(name: &str, requested: u32, products: &[Product]) -> Result<(), String> {
    let product = find_product(name, products)
        .ok_or_else(|| format!("找不到产品:{name}"))?;

    if product.stock < requested {
        return Err(format!("只剩 {} 个产品", product.stock));
    }
    Ok(())
}

ok_or_else 将 Option<&Product> 转换为带错误消息的 Result<&Product, String>,然后如果是 None 就用 ? 传播。这种写法避免了多层嵌套 match。

6. 用 thiserror 定义自定义错误

用 String 作为错误类型对小例子很方便,但真实项目需要结构化的错误 enum。Crate thiserror 使这变得简洁:

<!-- 相当于 Cargo.toml,使用 dependencies 部分 -->
# Cargo.toml
[dependencies]
thiserror = "1.0"
use thiserror::Error;

#[derive(Error, Debug)]
enum OrderError {
    #[error("产品不存在:{0}")]
    ProductNotFound(String),

    #[error("库存不足,剩余 {available} 个产品")]
    InsufficientStock { available: u32 },
}

fn validate_order(stock: Option<u32>, requested: u32) -> Result<(), OrderError> {
    let available = stock.ok_or_else(|| OrderError::ProductNotFound("Laptop".into()))?;
    if available < requested {
        return Err(OrderError::InsufficientStock { available });
    }
    Ok(())
}

#[derive(Error, Debug)] 自动生成 trait std::error::Error 的实现。#[error("...")] 为每个变体定义显示消息,支持如 {0}(位置)或 {available}(字段名)的占位符。打印错误时的结果:

Error: InsufficientStock { available: 2 }

如果打印时用 {} 而不是 {:?},你会得到友好的消息:库存不足,剩余 2 个产品。

7. 应用用 anyhow,库用 thiserror

当编写 binary/应用程序(不是供他人使用的库)时,你通常不需要区分错误类型的细节——只需传播和打印。Crate anyhow 适合这种情况:

[dependencies]
anyhow = "1.0"
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .with_context(|| format!("无法读取配置文件:{path}"))
}

fn main() -> Result<()> {
    let config = load_config("app.toml")?;
    println!("Config:{config}");
    Ok(())
}

anyhow::Result<T> 是 Result<T, anyhow::Error> 的别名,可以包含任何实现 trait Error 的错误。with_context 将上下文信息附加到原始错误,有助于调试而无需自己定义 enum。

  • 库(他人会 use 的 crate):使用 thiserror,返回具体的错误 enum 让调用者 match 处理各种情况。
  • 应用程序(最终 binary):使用 anyhow,只需传播和日志记录,没人需要详细的 match。

8. 测试错误行为

Rust 有内置的测试框架,基本测试无需额外的库。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tra_ve_ok_khi_du_hang() {
        let result = validate_order(Some(10), 5);
        assert!(result.is_ok());
    }

    #[test]
    fn tra_ve_err_khi_thieu_hang() {
        let result = validate_order(Some(2), 5);
        assert!(matches!(
            result,
            Err(OrderError::InsufficientStock { available: 2 })
        ));
    }

    #[test]
    fn tra_ve_err_khi_khong_tim_thay_san_pham() {
        let result = validate_order(None, 5);
        assert!(matches!(result, Err(OrderError::ProductNotFound(_))));
    }
}

#[test] 标记测试函数,用 cargo test 运行。matches! 是一个宏,检查值是否匹配给定的模式——比 assert_eq! 更方便处理有字段的 enum,因为你不需要为整个错误类型实现 PartialEq。

running 3 tests
test tests::tra_ve_ok_khi_du_hang ... ok
test tests::tra_ve_err_khi_thieu_hang ... ok
test tests::tra_ve_err_khi_khong_tim_thay_san_pham ... ok

9. 常见错误

  • 滥用 .unwrap():代码在开发时运行正常,但在生产环境中遇到陌生输入时会意外 panic。使用 ?、match 或 unwrap_or_else 进行明确处理。
  • 在公开库中用 String 作为错误类型:调用者无法按错误类型 match,只能比较字符串——当你改变消息时容易崩溃。用 thiserror 的 enum 代替 String。
  • 将 anyhow::Error 混入库的公开 API:隐藏了具体错误类型信息,调用者被迫手动 downcast。anyhow 只应在应用程序最后一层使用。
  • 忘记 #[derive(Debug)]:? 操作符和许多测试宏要求错误类型实现 Debug。缺少它,编译器会报告 trait bound 不满足的错误。

注意: 如果你来自 Java 并想“模拟 checked exception”,不要试图用宏模仿 try/catch。要习惯这样的思维:错误是值(value),像任何其他数据一样被返回和传递。

10. 结论

Option<T> 和 Result<T, E> 将“缺失”和“失败”的处理变成类型系统的一部分,由编译器在编译时检查而不是运行时。? 操作符使代码简洁而明确。对于小项目,String 或 anyhow 就足够了;对于共享库,值得投入用 thiserror 定义清晰的错误 enum,让调用者能安全地处理每种情况。

rust错误处理optionresultthiserror