Rust 错误处理

Rust 没有 try/catch,这是它和其他语言最显眼的区别之一。它把错误分成两类——不可恢复的 panic可恢复的 Result,分别用不同机制处理。这套设计让 Rust 代码的健壮性远超 try/catch 风格的语言,因为错误是类型,编译器强迫你处理。这一篇我们讲透。

1. 两类错误,两套机制

Rust 的错误处理哲学:

// Rust 把错误分成两类,用不同机制处理:

// 1. 不可恢复错误(panic)
//    程序进入了"坏状态",无法继续。用 panic! 触发,
//    会打印错误信息、展开栈、退出程序。
fn main() {
    let v = vec![1, 2, 3];
    // let x = v[10];    // 运行时 panic! 数组越界

    panic!("自定义崩溃");  // 主动触发 panic
}

// 2. 可恢复错误(Result)
//    操作可能失败,但有合理的恢复方式(如重试、提示用户)。
//    用 Result<T, E> 枚举表示:
//      Ok(T)  —— 成功,包含值 T
//      Err(E) —— 失败,包含错误 E
enum Result<T, E> {
    Ok(T),
    Err(E),
}

// 设计哲学:
// panic 用于"程序员的 bug"(数组越界、违反不变量)
// Result 用于"运行时可能失败的操作"(读文件、网络、解析)
// 没有 try/catch,错误是类型,必须显式处理

核心理解:

对比 try/catch:Java 把所有错误都用异常表示,你"可能"忘记 catch;Rust 把可恢复错误做成类型 Result,编译器强制你处理 Err 分支——不会"忘记"。

2. Result 类型

Result 是标准库提供的枚举(上一篇讲枚举时见过),Ok(T) 表示成功带值,Err(E) 表示失败带错误:

use std::fs;
use std::io;
use std::num::ParseIntError;

fn main() {
    // 打开文件可能失败:返回 Result
    let content: Result<String, io::Error> = fs::read_to_string("config.txt");

    // 必须显式处理 Err 的情况(类型系统强制)
    match content {
        Ok(text) => println!("文件内容: {}", text),
        Err(e) => eprintln!("读取失败: {}", e),
    }

    // 解析数字可能失败:返回 Result
    let n: Result<i32, ParseIntError> = "42".parse();
    match n {
        Ok(num) => println!("解析成功: {}", num),
        Err(e) => println!("解析失败: {}", e),
    }

    // 链式调用:map / and_then 处理成功情况
    let result = "100"
        .parse::<i32>()              // Result<i32, _>
        .map(|n| n * 2)              // 成功则乘以 2
        .map(|n| n + 1);
    println!("{:?}", result);        // Ok(201)
}

几个要点:

3. 问号运算符 ?(关键!)

这是 Rust 错误处理的杀手锏? 让错误传播像写同步代码一样自然——成功就解包值,失败就立即 return Err:

use std::fs;
use std::io;
use std::num::ParseIntError;

// ? 运算符:错误传播的语法糖
// 成功就解包出值,失败就立即返回 Err
fn read_config() -> Result<i32, io::Error> {
    // 不用 ? 的写法(啰嗦):
    let content = match fs::read_to_string("config.txt") {
        Ok(c) => c,
        Err(e) => return Err(e),     // 手动传播
    };

    // 用 ? 的写法(简洁!):
    let content = fs::read_to_string("config.txt")?;
    //  ↑ 等同上面的 match:
    //    成功 -> content 拿到 String
    //    失败 -> 函数立即 return Err(e)

    Ok(42)
}

// ? 能让多层错误传播像写同步代码一样自然
// 没有 ? 时:层层 match,代码又长又难看
// 有了 ?:像普通赋值一样,错误自动向上传播

// ? 还能做错误类型转换(From trait)
// 函数返回 io::Error,但 parse 返回 ParseIntError
// ? 会自动调用 From 把 ParseIntError 转成 io::Error
fn read_and_parse() -> Result<i32, io::Error> {
    let text = fs::read_to_string("num.txt")?;
    let n: i32 = text.trim().parse()?;      // ParseIntError -> io::Error
    Ok(n)
}

? 的魔力:

这是写 Rust 日常代码最重要的运算符之一。几乎所有可能失败的操作都用 ? 传播,代码读起来像没有错误处理的同步代码,但实际是健壮的。

4. unwrap 与 expect(谨慎使用)

unwrap 系列方法是 Result 的"快捷解包":成功拿值,失败 panic。它们简化代码牺牲健壮性:

fn main() {
    // unwrap:解包 Result,成功拿值,失败 panic
    let n: i32 = "42".parse().unwrap();      // OK,n = 42
    println!("{}", n);

    // let bad: i32 = "abc".parse().unwrap();  // panic!

    // expect:同 unwrap,但能自定义错误信息
    let n: i32 = "42"
        .parse()
        .expect("应该是数字啊");
    println!("{}", n);

    // unwrap_or / unwrap_or_else:失败时给默认值(不 panic)
    let n: i32 = "abc".parse().unwrap_or(0);          // 失败返回 0
    let n: i32 = "abc".parse().unwrap_or_else(|_| -1); // 失败返回 -1
    println!("{}", n);

    // 何时用 unwrap / expect?
    // - 写示例、原型、单元测试 -> 可以用
    // - 你 100% 确定不会失败 -> 可以用(但加注释说明)
    // - 生产代码、用户输入、IO 操作 -> 千万别用,用 ? 或 match
}

使用建议:

5. 自定义错误类型

当应用有多种错误来源(IO、解析、业务逻辑),通常定义一个统一错误类型。配合 From trait,让 ? 自动转换:

use std::fmt;
use std::error::Error;

// 自定义错误类型:实现 Debug + Display + Error trait
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
    Unauthorized,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO 错误: {}", e),
            AppError::Parse(e) => write!(f, "解析错误: {}", e),
            AppError::NotFound(name) => write!(f, "未找到: {}", name),
            AppError::Unauthorized => write!(f, "未授权"),
        }
    }
}

impl Error for AppError {}      // 默认实现就够

// 实现 From,让 ? 能自动转换
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) }
}

// 现在函数可以返回统一错误类型,? 自动转换各种子错误
fn do_work() -> Result<i32, AppError> {
    let text = std::fs::read_to_string("num.txt")?;   // io::Error -> AppError
    let n: i32 = text.trim().parse()?;                  // ParseIntError -> AppError
    Ok(n)
}

// 实战中推荐用 thiserror crate(自动生成上面的样板代码)
// 或 anyhow crate(用于应用层,不关心具体错误类型)

关键点:

6. thiserror 与 anyhow(实战利器)

两个最流行的错误处理 crate:

经验:库的代码用 thiserror 提供精确的错误类型;应用代码用 anyhow 简化错误传播。这是 Rust 社区的常见组合。

7. panic 详解

panic 表示"不可恢复",触发后会展开调用栈(默认)或直接终止。看下细节:

fn main() {
    // panic! 触发不可恢复错误
    // 适用场景:
    // - 数组越界、违反不变量(理论上不该发生)
    // - 程序员逻辑错误(不是用户输入问题)
    // - 初始化失败(程序无法继续运行)

    let v = vec![1, 2, 3];
    if let Some(x) = v.get(10) {
        println!("{}", x);
    } else {
        // 这里如果继续运行会出错,可以 panic
        // panic!("数组索引错误");
    }

    // 常见自动 panic 的场景:
    // - 数组越界 v[10] (用 .get() 避免)
    // - unwrap 失败
    // - 整数溢出(debug 模式)
    // - 除以零
    // - 显式 unreachable!() / todo!() / unimplemented!()

    // panic 的行为:
    // - 默认:展开栈(unwind),清理资源
    // - 可配置:直接终止(abort),体积更小
    //   在 Cargo.toml: [profile.release] panic = "abort"
}

// panic vs Result 怎么选?
// - "用户的错"(输入错、文件不存在、网络断) -> Result
// - "程序员的错"(逻辑不可能出现的情况) -> panic

panic 的两种栈处理:

8. 错误处理最佳实践

9. 实战:写一个健壮的文件读取

综合应用,这是典型场景:

小结

这一篇你掌握了 Rust 的错误处理体系:panic(不可恢复,程序员错误)、Result<T, E>(可恢复,类型化错误)、问号 ?(优雅传播)、unwrap/expect(谨慎用)、自定义错误 + From(统一错误处理)、thiserror/anyhow(实战利器)。Rust 的"错误即类型"哲学,让代码的健壮性远超 try/catch 风格——错误不会被"忘记处理"。

下一篇是系列最后一篇——模块与 Crate,讲解 Rust 如何组织大型项目的代码。

← 上一篇 Rust Trait 特征

下一篇 Rust 模块与 Crate

✈️💬