ToolkitX
知识库工具箱

错误处理

Result, Option, ? 运算符, 自定义错误

20min·进阶

01. panic! 和不可恢复错误

panic! 触发不可恢复错误,程序展开栈(回退)并崩溃。Rust 分为两种 panic 策略:unwind(展开栈,清理资源)和 abort(直接退出,不清理)。开发期常用 unwrap() 和 expect(),它们遇到 None 或 Err 就 panic。产品代码应尽量用 Result 传播错误而不是 unwrap。
rust
fn main() {
    // 直接 panic
    // panic!("致命的错误");
    
    let v = vec![1, 2, 3];
    
    // unwrap: 如果是 None/Err 就 panic
    let first = v.get(0).unwrap();
    
    // expect: 带自定义信息的 unwrap
    let second = v.get(1).expect("索引 1 不存在");
    
    // 更好的写法:用 match 或 ? 传播
    match v.get(5) {
        Some(val) => println!("{}", val),
        None => println!("元素不存在"),
    }
}
库代码里永远不要 unwrap,让调用方决定怎么处理。应用代码的 unwrap 也要考虑能否换成更好的错误处理。

02. Result 和错误传播

Result<T, E> 是 Rust 标准错误处理方式,定义在标准库预引入里。Ok 代表成功携带值,Err 代表失败携带错误信息。? 操作符是 Rust 错误处理的核心:Ok 时提取值,Err 时提前返回错误。注意 ? 会自动调用 From trait 转换错误类型。
rust
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

// ? 链式调用
fn read_username_short() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

// 更短:标准库一行搞定
fn read_username_shortest() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
? 运算符可以在返回 Result 或 Option 的函数中使用,而且会自动调用 into() 转换错误类型。

03. 自定义错误类型

复杂项目需要自定义错误类型。用枚举包裹各种可能的错误,实现 Display 和 Debug trait。thiserror 库可以自动派生 Display 和 From 实现,减少模板代码。用 #[from] 属性让 ? 运算符自动转换子错误类型。
rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataError {
    #[error("数据库错误: {0}")]
    Database(#[from] sqlx::Error),
    
    #[error("未找到数据: {0}")]
    NotFound(String),
    
    #[error("验证失败: {field} - {message}")]
    Validation { field: String, message: String },
}

fn find_user(id: i32) -> Result<User, DataError> {
    let user = db.query("...")?  // sqlx::Error 自动转 DataError::Database
        .ok_or(DataError::NotFound(format!("用户 {} 不存在", id)))?;
    Ok(user)
}
thiserror 让自定义错误几乎零代码,只需要定义枚举变体和错误信息模板。

04. anyhow 用于应用级错误处理

anyhow 提供了一个统一的错误类型 anyhow::Error,适合应用层(不是库),不需要定义复杂的错误枚举。Context trait 给 Result 加上下文信息,with_context 添加描述,链式附加上下文。bail! 宏相当于 return Err。适合快速开发和脚本。
rust
use anyhow::{Context, Result, bail};

fn read_config() -> Result<String> {
    let path = "config.toml";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("无法读取配置文件 {}", path))?;
    Ok(content)
}

fn validate_age(age: i32) -> Result<()> {
    if age < 0 {
        bail!("年龄不能为负数: {}", age);
    }
    if age > 150 {
        bail!("年龄太大了: {}", age);
    }
    Ok(())
}

fn main() -> Result<()> {
    let config = read_config()?;
    println!("{}", config);
    Ok(())
}
库用 thiserror,应用用 anyhow,这是 Rust 社区的流行实践。

05. Option 和 Result 的便捷方法

Result 和 Option 提供了丰富的组合方法链式处理。ok_or 把 Option 转 Result,ok 反之。map/map_err 变换成功和失败值。and_then/or_else 链式组合。unwrap_or 提供默认值,unwrap_or_else 惰性计算默认值。这些方法让错误处理代码更函数式更简洁。
rust
fn main() {
    let opt = Some(3);
    
    let val = opt.unwrap_or(0);                              // 3
    let val = None.unwrap_or_else(|| expensive_default());    // 惰性
    let doubled = opt.map(|x| x * 2);                        // Some(6)
    let filtered = opt.filter(|&x| x > 5);                   // None
    
    let res: Result<i32, &str> = Ok(10);
    
    let val = res.unwrap_or(0);                              // 10
    let mapped = res.map(|x| x * 2);                         // Ok(20)
    let err_mapped = res.map_err(|e| format!("!{}", e));    // Ok(10)
    
    // 链式处理
    let user_id = Some(42);
    let username = user_id
        .and_then(|id| find_user(id))
        .map(|u| u.name)
        .unwrap_or_else(|| "未知用户".to_string());
}
unwrap_or_else 比 unwrap_or 好,因为默认值只在需要时才计算,不会做无用功。

知识测验

1/4正确 0

Rust 中 unwrap() 的作用?

下一节

Trait 与泛型

下一节