1338 字
7 分钟
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);
}

执行结果:

Terminal window
尝试解析 '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 函数中使用 ? 来统一处理来自不同模块的异构错误,极大地简化了顶层错误处理逻辑。

Rust语言学习:错误处理
https://haoyn231.github.io/posts/rust/rust语言学习错误处理/
作者
好软好温暖
发布于
2025-08-27
许可协议
CC BY-NC-SA 4.0
评论