Chapter 06

错误处理哲学

Rust 将错误处理融入类型系统——让可恢复错误显式化,让不可恢复错误快速失败

错误的两种类别

Rust 的错误哲学

Rust 将错误明确分为两类,并提供不同的处理机制:

不可恢复错误(panic)
程序遇到了无法处理的状态,继续运行会导致数据损坏或安全问题。Rust 选择立即终止(或 unwind 栈),打印错误信息。例如:数组越界、整数溢出(debug 模式)、unwrap None 值。
可恢复错误(Result)
预期中可能发生的失败情况,调用方应当处理。例如:文件不存在、网络连接失败、解析错误。用 Result<T, E> 枚举表示。

这种设计的哲学是:不要用异常机制(try/catch)混淆这两类错误。可恢复错误应该是函数签名的一部分,调用方必须显式处理;不可恢复错误则直接让程序崩溃,避免在损坏的状态下继续运行。

panic!:不可恢复错误

何时触发 panic

fn main() {
    // 显式 panic
    panic!("这里出了问题!");

    // 这些操作在 debug 模式下会 panic:
    let v = vec![1, 2, 3];
    v[99]; // 越界访问 → panic!

    // unwrap() 在 None 或 Err 时 panic
    let x: Option<i32> = None;
    x.unwrap(); // panic: called `Option::unwrap()` on a `None` value

    // expect() 类似 unwrap(),但可以自定义错误信息
    x.expect("x 应该有值"); // panic: x 应该有值
}
何时可以用 unwrap/expect?

在生产代码中应尽量避免 unwrap(),因为一旦值是 None/Err 就会 panic 导致服务崩溃。合理的使用场景:
1. 原型代码和学习示例(快速迭代)
2. 测试代码中(panic 本来就代表测试失败)
3. 当你有 100% 把握某个值不会是 None/Err 时,用 expect("不可能是 None,因为...") 加上原因说明

Result<T, E>:可恢复错误

Result 的定义与使用

// Result 的定义(来自标准库):
enum Result<T, E> {
    Ok(T),   // 操作成功,携带结果值
    Err(E),  // 操作失败,携带错误信息
}

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

fn read_username_from_file() -> Result<String, io::Error> {
    let result = fs::read_to_string("username.txt");

    match result {
        Ok(content) => Ok(content.trim().to_string()),
        Err(e)      => Err(e),
    }
}

fn main() {
    match read_username_from_file() {
        Ok(name) => println!("用户名: {}", name),
        Err(e)   => println!("读取失败: {}", e),
    }
}

? 运算符:优雅的错误传播

? 运算符是 Rust 中处理 Result 的语法糖:如果值是 Ok(v),则 ? 提取 v;如果是 Err(e),则将 Err(e) 立即从当前函数返回(相当于提前 return Err(e))。

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

// 用 ? 运算符简化错误传播
fn read_username() -> Result<String, io::Error> {
    let content = fs::read_to_string("username.txt")?;
    //                                              ↑ 如果 Err,立即 return Err
    Ok(content.trim().to_string())
}

// 链式 ? 调用
fn read_and_parse() -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string("number.txt")?;  // io::Error
    let number: i32 = content.trim().parse()?;        // ParseIntError
    Ok(number * 2)
}

fn main() {
    // main 函数也可以返回 Result!
    if let Ok(num) = read_and_parse() {
        println!("数字的两倍: {}", num);
    }
}
? 运算符的自动类型转换

? 运算符还会自动调用 From::from() 将错误类型转换为函数返回类型中声明的错误类型(前提是实现了 From trait)。这使得在一个函数中处理多种不同的错误类型变得简单。

自定义错误类型

手动实现 Error trait

use std::fmt;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    InvalidInput(String),
}

// 实现 Display trait(用于用户友好的错误信息)
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e)       => write!(f, "I/O 错误: {}", e),
            AppError::Parse(e)    => write!(f, "解析错误: {}", e),
            AppError::InvalidInput(s) => write!(f, "无效输入: {}", s),
        }
    }
}

// 实现 Error trait
impl std::error::Error for AppError {
    fn source(&self) -> Option<&dyn std::error::Error> {
        match self {
            AppError::Io(e)    => Some(e),
            AppError::Parse(e) => Some(e),
            _                  => None,
        }
    }
}

// 实现 From trait,支持 ? 运算符自动转换
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

thiserror:用宏简化自定义错误

手动实现 Error trait 很繁琐。thiserror crate 提供过程宏,让你用几行代码定义完整的错误类型:

[dependencies]
thiserror = "1"
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("I/O 错误: {0}")]
    Io(#[from] std::io::Error),

    #[error("解析错误: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("无效输入: {message}")]
    InvalidInput { message: String },

    #[error("未找到配置项 '{key}'")]
    ConfigNotFound { key: String },
}
// thiserror 自动生成 Display、Error、From 实现!

anyhow:应用层的便捷错误处理

anyhow 适用于应用程序(而非库),它提供了一个通用的 anyhow::Error 类型,可以容纳任何错误:

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

fn process_config(path: &str) -> Result<String> {
    // context() 为错误添加上下文信息
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("无法读取配置文件 {}", path))?;

    let value: i32 = content.trim().parse()
        .context("配置值必须是整数")?;

    Ok(format!("配置值的两倍: {}", value * 2))
}

fn main() -> Result<()> {
    let result = process_config("config.txt")?;
    println!("{}", result);
    Ok(())
}
库 vs 应用的错误处理策略

库(library):使用自定义错误类型(配合 thiserror),让调用方能够精确地 match 错误类型并决定如何处理。不要在库中使用 anyhow::Error,它会擦除类型信息。
应用程序(application):使用 anyhow,追求简洁,专注于给用户好的错误信息和调试体验,而不是对每种错误做精细处理。

Result 的常用方法

fn main() {
    let ok: Result<i32, &str>  = Ok(42);
    let err: Result<i32, &str> = Err("出错了");

    // 提取值(失败时 panic)
    ok.unwrap();              // 42
    ok.expect("msg");         // 42

    // 提供默认值
    err.unwrap_or(0);         // 0
    err.unwrap_or_default(); // 0(i32 的默认值)
    err.unwrap_or_else(|e| { println!("错误: {}", e); -1 });

    // 变换值
    ok.map(|v| v * 2);         // Ok(84)
    err.map_err(|e| e.len()); // Err(4)(变换错误类型)

    // 检查状态
    ok.is_ok();               // true
    err.is_err();             // true

    // and_then:链式处理(只有 Ok 时才执行)
    ok.and_then(|v| if v > 10 { Ok(v) } else { Err("太小") });
}