Skip to content

Day 9: 错误处理

今天是《30天精通Rust语言》系列的第九天。我们将学习Rust的错误处理机制。Rust采用显式错误处理策略,没有异常机制,所有可能失败的操作用 Result 类型表示,需要显式处理错误。

Result 是Rust标准库中用于表示操作结果的枚举类型:

enum Result<T, E> {
Ok(T), // 成功,持有成功值
Err(E), // 失败,持有错误值
}

与 Option<T> 类似,但 Result 可以携带错误信息。

use std::fs::File;
use std::io::Read;
fn main() {
let result = File::open("hello.txt");
match result {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
println!("File contents: {}", contents);
}
Err(error) => {
println!("Error opening file: {:?}", error);
}
}
}
fn main() {
let result: Result<i32, &str> = Ok(42);
// unwrap:成功返回Ok的值,失败panic
let n = result.unwrap();
println!("n = {}", n);
// unwrap_or:失败时返回默认值
let result: Result<i32, &str> = Err("error");
let n = result.unwrap_or(0);
println!("n = {}", n);
// is_ok和is_err:判断结果类型
let result: Result<i32, &str> = Ok(42);
println!("is_ok: {}, is_err: {}", result.is_ok(), result.is_err());
}

? 运算符是Rust中处理错误的利器,可以简化错误传播:

use std::fs::File;
use std::io::Read;
fn read_file_contents(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // ?运算符
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file_contents("hello.txt") {
Ok(contents) => println!("{}", contents),
Err(e) => println!("Error: {:?}", e),
}
}

? 运算符的展开等价于以下 match 表达式:

// ?运算符写法
let mut file = File::open(path)?;
// 等价于
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e.into()),
};

如果结果是 Ok,展开并继续执行;如果是 Err,提前返回,将错误传播给调用者。

当程序遇到无法恢复的错误时,使用 panic! 终止程序:

fn main() {
panic!("This is a critical error!");
println!("This will never be printed");
}

运行时会输出:

thread 'main' panicked at 'This is a critical error!', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` ...
  • 不可恢复错误:如预期不可能发生的情况(使用 unreachable! 更好)。
  • 原型开发:快速原型中可使用 unwrap(),生产代码应使用 ?。
  • 测试代码:测试中验证某操作应该失败。

在开发中快速获取值的方法:

fn main() {
let result: Option<i32> = Some(42);
// unwrap:Some返回值,None panic
let n = result.unwrap();
println!("n = {}", n);
// expect:提供错误信息
let n = result.expect("Expected a value");
println!("n = {}", n);
}

对于实际应用,你应该定义自己的错误类型:

use std::fmt;
use std::num::ParseIntError;
// 定义错误类型
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(ParseIntError),
InvalidInput(String),
}
//实现Display trait
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(e) => write!(f, "Parse error: {}", e),
AppError::InvalidInput(s) => write!(f, "Invalid input: {}", s),
}
}
}
// 实现Error trait
impl std::error::Error for AppError {}
// From trait用于?运算符自动转换
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::IoError(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::ParseError(err)
}
}
fn parse_and_double(s: &str) -> Result<i32, AppError> {
let n: i32 = s.trim().parse::<i32>()?;
if n < 0 {
return Err(AppError::InvalidInput("Negative numbers not allowed".to_string()));
}
Ok(n * 2)
}
fn main() {
let cases = ["42", "hello", "-5"];
for s in cases {
match parse_and_double(s) {
Ok(n) => println!("{} * 2 = {}", s, n),
Err(e) => println!("Error processing '{}': {}", s, e),
}
}
}

Result 类型提供了丰富的组合器方法:

fn main() {
let result: Result<i32, &str> = Ok(5);
// map:转换Ok值
let doubled = result.map(|x| x * 2);
println!("doubled: {:?}", doubled);
// map_err:转换Err值
let result: Result<i32, &str> = Err("error");
let mapped = result.map_err(|e| e.len());
println!("mapped: {:?}", mapped);
// and_then:链式处理
let result: Result<i32, &str> = Ok(5);
let chained = result.and_then(|x| Ok(x * 2));
println!("chained: {:?}", chained);
}

对于应用程序,anyhow crate 提供了更灵活的错误处理:

Cargo.toml
[dependencies]
anyhow = "1.0"
use anyhow::Result;
fn read_config(path: &str) -> Result<String> {
let contents = std::fs::read_to_string(path)?;
Ok(contents)
}
fn main() -> Result<()> {
let config = read_config("config.toml")?;
println!("Config: {}", config);
Ok(())
}

今天的实践任务:

  1. 编写程序打开一个不存在的文件,使用 match 打印成功或失败消息。
  2. 使用 ? 运算符重写上述程序。
  3. 创建一个自定义错误类型 DivisionError,包含”除以零”和”结果非整数”两种错误。
  4. 实现一个字符串解析函数,解析后使用 ? 传播错误。
  5. 使用 Result 的组合器方法 map 和 and_then 实现链式计算。

问:什么时候应该panic而不是返回Result?

答:当错误表示程序中存在无法恢复的状态时使用panic,如数组越界(已由Rust标准库检查)、契约违反等。返回Result适用于调用者可能需要处理的错误。

问:?运算符能否用于Option?

答:可以,但需要确认值存在时才能用。? 用于Option时会尝试获取值,如果为None则提前返回None。

今天我们学习了Rust的错误处理机制:

  • Result<T, E> 是表示可能失败操作的枚举类型。
  • ? 运算符简化了错误的传播和处理。
  • panic! 用于不可恢复的错误,终止程序。
  • 自定义错误类型提供了更丰富的错误信息。
  • 组合器和第三方库(如 anyhow)简化了错误处理代码。

明天我们将学习Rust的最后一个核心概念:生命周期。加油!