# Rust 错误处理机制

# 一、 错误分类:可恢复与不可恢复

在 Rust 中,错误被分为两大类:不可恢复的错误和可恢复的错误。系统为这两种错误提供了不同的处理机制。

特性 panic! (不可恢复错误) Result<T, E> (可恢复错误)
核心用途 处理程序缺陷(Bug),表示程序进入了无法安全继续执行的状态。 处理可预期的、能够被合理响应的运行时错误。
典型场景 访问数组越界、断言失败等。 文件未找到、网络连接中断、输入解析失败等。
程序行为 立即终止当前线程,展开调用栈并清理数据。 返回一个包含成功值或错误值的枚举,由调用者决定如何处理。
触发方式 1. 显式调用 panic!("message"); 2. 代码执行了致命错误操作。 函数显式地返回 Ok(value)Err(error)

# 二、 Result<T, E> 枚举:显式处理错误

Result<T, E> 是一个标准库定义的枚举,专门用于处理可恢复的错误。它有两个变体:

  • Ok(T): 表示操作成功,并包含成功时返回的值,其类型为泛型 T
  • Err(E): 表示操作失败,并包含一个描述错误的值,其类型为泛型 E

# 1. 示例:解析字符串

use std::num::ParseIntError;

// 函数尝试将字符串解析为 i32
// 成功时返回 Ok(i32),失败时返回 Err(ParseIntError)
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse::<i32>()
}

fn main() {
    let success = parse_number("123");
    let failure = parse_number("abc");

    println!("尝试解析 '123': {:?}", success);
    println!("尝试解析 'abc': {:?}", failure);
}

执行结果:

尝试解析 '123': Ok(123)
尝试解析 'abc': Err(ParseIntError { kind: InvalidDigit })

# 2. 使用 match 表达式处理

match 是处理 Result 最基础、最明确的方式。Rust 的 match 是穷尽的(exhaustive),编译器会强制你处理所有可能的情况(OkErr),确保错误不会被意外忽略。

fn main() {
    let number_str = "42";
    
    match "42".parse::<i32>() {
        // 如果结果是 Ok,`n` 会绑定到 Ok 内部的值
        Ok(n) => println!("解析成功!数字是: {}", n),
        // 如果结果是 Err,`e` 会绑定到 Err 内部的错误
        Err(e) => println!("解析失败!错误是: {:?}", e),
    }

    match "hello".parse::<i32>() {
        Ok(n) => println!("解析成功!数字是: {}", n),
        Err(e) => println!("解析失败!错误是: {:?}", e),
    }
}

# 三、 ? 运算符:简洁的错误传播

在实际开发中,函数内部常常会调用多个可能失败的操作。如果每个操作都使用 match 处理,代码会变得冗长和嵌套。? 运算符就是为了解决这个问题而生的,它用于实现错误的 传播(Propagation),即如果内部调用失败,则立即将错误返回给当前函数的调用者。

  • 核心功能:当 Result 的值为 Ok(T) 时,它会解包出 T 并让程序继续;如果值为 Err(E),它会立刻中断当前函数的执行,并将 Err(E) 作为当前函数的返回值。
  • 链式调用? 运算符可以使一系列可能失败的操作代码保持线性、清晰和易读。
use std::fs::File;
use std::io::{self, Read};

// 链式调用示例:打开文件并读取其所有内容
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    
    // 1. File::open 返回 Result<File, io::Error>
    //    `?` 在成功时返回 File 实例,失败时将 io::Error 返回
    // 2. .read_to_string 返回 Result<usize, io::Error>
    //    `?` 在成功时返回读取的字节数,失败时将 io::Error 返回
    File::open("username.txt")?.read_to_string(&mut username)?;
    
    Ok(username)
}

# 四、 ? 运算符的工作机制

# 1. 使用前提:函数的返回类型

? 运算符只能在返回类型为 Result<T, E>Option<T> 或其他实现了内部 FromResidual Trait 的函数中使用。这是因为 ? 的核心行为是“提前返回一个 ErrNone”,如果当前函数的签名不允许返回这样的类型,编译器就会报错。

错误示例main 函数默认返回 (),不能使用 ?

use std::fs::File;

fn main() {
    // 编译错误!
    // error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
    let f = File::open("hello.txt")?; 
}

# 2. 错误类型的自动转换

? 运算符还有一个强大的功能:如果它遇到的错误类型 E1 与当前函数签名的错误类型 E2 不同,它会尝试使用 From Trait 将 E1 自动转换为 E2。这是通过 E2::from(E1) 实现的。这在处理来自不同库的多种错误类型时非常有用。

# 五、 应用程序顶层的错误处理

从 Rust 1.26 版本开始,main 函数可以返回一个 Result,只要其错误类型实现了 std::process::Termination Trait(std::error::Error Trait 的实现者通常也满足要求)。最常见的实践是让 main 返回 Result<(), Box<dyn Error>>

  • () 作为成功类型:表示程序成功运行到结束,没有特定的返回值。
  • Box<dyn Error> 作为错误类型:这是一个 Trait 对象,可以容纳任何实现了 std::error::Error Trait 的错误类型。

为什么这样设计?
一个复杂的程序可能会遇到多种错误,例如 io::Error(文件操作)、serde_json::Error(JSON 解析)或 reqwest::Error(网络请求)。

借助 ? 运算符的自动类型转换能力,任何具体的错误类型(如 io::Error)都可以被自动转换并装箱(Box)成一个 Box<dyn Error>。这使得我们可以在 main 函数中使用 ? 来统一处理来自不同模块的异构错误,极大地简化了顶层错误处理逻辑。