两类错误
在 Rust 中,我们将错误分为两大类:可恢复错误与不可恢复错误。
-
可恢复错误,比如文件未找到等,一般需要将它们报告给用户并再次尝试进行操作。
-
不可恢复错误,往往就是 bug 的另一种说法,比如尝试访问超出数组结尾的位置等,我们希望在这种情形下 立即停止程序。
Rust 没有异常机制,对于可恢复的错误,提供了 Result<T,E>,对于不可恢复的错误,Rust 提供了 panic!。
panic
当 panic 发生时,程序会默认开始执行栈展开。这意味着 Rust 会沿着调用栈的反向顺序遍历所有的调用函数,并依次清理这些函数中的数据。
如果想直接终止,可以在 Cargo.toml 中的 [profile] 中添加
[profile.release]
panic = 'abort'
失败时触发 panic
Result<T,E> 提供了一个 unwrap 的方法实现了一个 match 效果。如果返回 Ok 变体,unwrap 方法就会返回 Ok 内部的值。
返回 Err 时,则会自动调用 panic!。
let mut greeting_file = File::open("hello.txt").unwrap();
except 和 unwrap 提供了同样的功能,但是会传入参数字符串作为错误信息输出。unwrap 只会携带默认的信息。
在实际的生产环境中,绝大多数的 Rust 开发者 都会选择使用 expect 而不是 unwrap,因为它可以提供更多的信息来描述操作为什么应该是成功的。
可恢复错误
大部分错误其实都没有严重到需要整个程序停止运行的地步。
例如,尝试打开文件的操作会因为文件不存在而失败。在这种情形下,你也许会考虑创建该文件而不是中止进程
enum Result<T,E> {
Ok(T),
Err(E),
}
这里的 T 和 E 是泛型参数。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let _ = match greeting_file_result {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error);
}
};
}
匹配不同的错误
如果想要匹配不同错误,可以增加内部的 match:
use std::fs::File;
use std::io::{ErrorKind, Read};
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!(
"Problem creating the file: {:?}",
e
)
}
},
other_error => {
panic!(
"Problem opening the file: {:?}",
other_error
);
}
}
};
}
使用闭包处理错误
可以使用 unwrap_or_else 方法更加简洁地处理代码出现的 Result<T,E>:
use std::fs::File;
use std::io::{ErrorKind};
fn main() {
let _greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
传播错误
除了可以在函数内部处理错误,我们还可以将这个错误返回给调用者,让其决定应该如何做进一步处理。这个过程也被称作 传播错误(propagating error)。
手动传播错误
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
这个函数尝试从文件中读取用户名。如果文件不存在或无法读取,函数会将错误返回给调用者。
? 运算符简化错误传播
Rust 提供了 ? 运算符来简化错误传播的代码:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
? 运算符的工作原理:
- 如果
Result是Ok,则返回Ok中的值,程序继续执行 - 如果
Result是Err,则会提前返回整个函数,将错误传播给调用者
甚至可以进一步简化:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
或者使用更简洁的 fs::read_to_string:
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
? 运算符的限制
? 运算符只能在返回 Result 或 Option 的函数中使用。在 main 函数中使用 ? 需要将返回类型改为 Result:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
这里的 Box<dyn Error> 是一个 trait 对象,表示任何类型的错误。
自定义错误类型
在实际项目中,我们常常需要定义自己的错误类型:
use std::fmt;
#[derive(Debug)]
pub enum MyError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
CustomError(String),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::IoError(err) => write!(f, "IO error: {}", err),
MyError::ParseError(err) => write!(f, "Parse error: {}", err),
MyError::CustomError(msg) => write!(f, "Custom error: {}", msg),
}
}
}
impl std::error::Error for MyError {}
// 实现 From trait 以支持 ? 运算符的自动转换
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> MyError {
MyError::IoError(err)
}
}
impl From<std::num::ParseIntError> for MyError {
fn from(err: std::num::ParseIntError) -> MyError {
MyError::ParseError(err)
}
}
使用自定义错误类型:
use std::fs;
fn process_file(path: &str) -> Result<i32, MyError> {
let contents = fs::read_to_string(path)?;
let number: i32 = contents.trim().parse()?;
if number < 0 {
return Err(MyError::CustomError("Number must be positive".to_string()));
}
Ok(number * 2)
}
使用第三方库简化错误处理
anyhow - 简化应用程序错误处理
对于应用程序开发,anyhow 提供了简单的错误处理方案:
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.context("Failed to read config file")
}
thiserror - 简化库的错误定义
对于库开发,thiserror 可以大幅简化自定义错误类型的代码:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] std::io::Error),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
何时使用 panic! vs Result
使用 panic! 的场景:
- 示例代码、原型或测试 - 在快速原型开发时使用
unwrap或expect - 不可能失败的情况 - 当你确定代码逻辑上不会失败时
let home: IpAddr = "127.0.0.1".parse().unwrap(); - 违反约定的情况 - 当接收到无效参数时
使用 Result 的场景:
- 预期可能发生的错误 - 如文件不存在、网络连接失败
- 可以恢复的错误 - 调用者可能有处理或重试的策略
- 库代码 - 让调用者决定如何处理错误
最佳实践
-
在库中返回
Result,在应用中决定是否panic -
提供有意义的错误信息
File::open("config.toml") .expect("Failed to open config.toml - make sure it exists in the project root") -
使用自定义错误类型提供结构化信息
-
考虑错误恢复策略 - 是重试、降级服务还是返回默认值?
-
不要忽略错误 - 避免使用
let _ = ...忽略Result
总结
panic!宏表示程序正处于一个无法处理的状态下,你需要中止进程运行,而不是基于无效或非法的值继续执行命令。Result枚举可以借助 Rust 的类型系统表明某个操作有失败的可能,并且代码能够从这种失败中恢复过来。?运算符大大简化了错误传播的代码,使代码更加简洁易读。- 自定义错误类型可以提供更丰富的错误信息和更好的类型安全。
- 第三方库如
anyhow和thiserror可以进一步简化错误处理代码。 - 选择
panic!还是Result取决于错误是否可恢复以及调用者是否需要处理它。