哈喽,大家好呀,我是呼噜噜,今天我们来聊聊Rust错误处理,这个也是Rust陡峭学习曲线中的一座大山,让笔者给你讲解一下什么是Rust错误处理
与 Java、Python 或 C++ 使用异常Exceptions(引入了try-catch 机制),不同的是,Rust 将错误视为值,并且强制开发者显式地处理它们。
也就是说:如果你可能失败,你就必须在类型系统中显式地声明这一点,并且调用者必须显式地处理它
Rust错误类型
在Rust中,错误处理的设计目标是兼顾性能与安全,所以错误被分为2大类:
- 不可恢复错误
指程序发生了严重的、致命的问题程序。比如:访问数组越界、解引用空指针、递归时栈溢出、使用unwrap()或expect()在None或Err等等
当程序发生了上述的情况,rust就会触发panic 。panic 是 Rust 用来处理不可恢复错误的一种机制,触发后,会导致程序立即停止执行,并展开调用栈,最终终止程序
panic除了被动触发,还可以手动触发,Rust 标准库提供了 panic! 宏,我们就可以利用它来手动触发
我们来看个小例子:
fn main() {
panic!("出错啦!这是一个 panic 示例。");
}程序运行到这里会直接终止,并打印错误信息和调用栈。
另外了解一下.unwrap()和.expect(msg),如果发生错误,它们也是返回panic!,区别是expect允许你自定义 panic 的错误信息
在生产环境代码中,我们都应尽量避免被动触发或者手动触发 panic,这次的Cloudflare全球宕机事件就是血泪教训!

- 可恢复错误
指程序遇到问题时,但可以通过某种方式处理错误,允许程序继续运行。比如:文件未找到、网络超时、解析输入失败、权限不足等等
一般情况下,我们推荐使用 Result 和 Option 进行可恢复错误处理,而不是panic。故接下来我们重点来讲解可恢复的错误处理
Result 与 Option
我们还必须知道,Rust是没有 null的,也没有 Exception的。它通过自身的类型系统,来强制我们在编译阶段就得显式地处理所有可能的情况。
这样可以从根源上消除了空指针异常并让错误处理兼顾性能与安全
虽然这样写起代码,起初会觉得比较繁琐,但能极大地提高代码的健壮性,另外你可别担心,当熟练🦀后,掌握各种丰富且友好的语法糖,就能极大地提升你的开发效率。
Option
Option<T> 是一个表示 可能有值Some(T) 或 没有值None 的枚举类型:
enum Option<T> {
Some(T), // 包含一个值 T
None, // 什么都没有
}它可以解决 "Null Pointer Exception"(空指针异常),这价值"十亿美元"的错误
常用方法:map, and_then(或 flat_map),unwrap_or, unwrap_or_else,ok_or
我们来看一个例子:
fn find_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return Some(n);
}
}
None
}
let nums = vec![1, 3, 5];
let result = find_even(&nums);
let nums2 = vec![1, 2, 3];
let result2 = find_even(&nums2);上述,result 是 None,而result2是Some(2)
Result<T, E>
Result 用于表示 可能成功或失败 的操作。它定义如下:
enum Result<T, E> {
Ok(T), // 成功,包含成功的值 T
Err(E), // 失败,包含错误的类型 E
}它是 Rust 处理可恢复错误的核心类型。E 通常实现 std::error::Error trait,用于错误链与显示。
我们再来看一个例子:
use std::fs::File;
fn open_file(path: &str) -> Result<File, std::io::Error> {
File::open(path)
}
let file = open_file("hello.txt");上述代码,如果文件存在,返回 Ok(File),如果文件不存在,则返回 Err(std::io::Error)
用match来处理Result和Option
在Rust中,我们可以用match模式匹配来处理Result和Option
match result2 {
Some(val) => println!("找到偶数: {}", val),
None => println!("没有偶数"),
}
match file {
Ok(f) => println!("文件打开成功!"),
Err(e) => println!("文件打开失败: {}", e),
}虽然 match 很安全,但在实际业务中,我们往往需要连续执行多个可能出错的操作。如果每一步都用 match,代码会变成“右箭头型”的嵌套地狱。

我们来看看下方的这个例子:
fn get_reciprocal(input: &str) -> Result<f64, String> {
//match 嵌套
match input.parse::<f64>() {
Ok(num) => {
match safe_divide(1.0, num) {
Ok(res) => Ok(res),
Err(e) => Err(e),
}
},
Err(_) => Err(String::from("解析数字失败")),
}
}
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, String> {
if divisor == 0.0 {
return Err(String::from("除数不能为零"));
}
Ok(dividend / divisor)
}
fn main() {
let inputs = vec!["2.0", "0.0", "abc"];
for s in inputs {
println!("{:?} -> {:?}", s, get_reciprocal(s));
}
}上述代码,主要是判断值是否可以进行倒数运算。整体逻辑是非常简单的,但是代码已经很繁琐了,特别是里面套了2层match
那我们改如何优化呢?

使用组合子来优化我们的错误处理
组合子Combinators的概念在函数式编程中很常见,Rust吸收了很多函数式编程的理念,为 Result 和 Option 提供了如 map, and_then, unwrap_or 等方法。
fn get_reciprocal(input: &str) -> Result<f64, String> {
// 使用组合子
input.parse::<f64>()
.map_err(|_| String::from("解析数字失败"))
.and_then(|num| safe_divide(1.0, num))
}
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, String> {
if divisor == 0.0 {
return Err(String::from("除数不能为零"));
}
Ok(dividend / divisor)
}
fn main() {
let inputs = vec!["2.0", "0.0", "abc"];
for s in inputs {
println!("{:?} -> {:?}", s, get_reciprocal(s));
}
}结果:
"2.0" -> Ok(0.5)
"0.0" -> Err("除数不能为零")
"abc" -> Err("解析数字失败")这样我们将错误处理逻辑链式调用,消除了嵌套,代码也更加简洁了
这个时候,有读者朋友就会说了,俺不喜欢链式写法,感觉不够直观,还有什么好办法?

有的兄弟有的,Rust还给我们提供?,且让我下文慢慢道来
错误传播:? 运算符
Rust在2018 引入了 ? 操作符,它是语法糖,也是 Rust 错误处理最优雅的设计之一。
简单来说?实现了错误传播。它常用于简化 Result、Option 的错误处理,让我们的代码更清晰、更简洁
在实际开发中,我们通常不在当前函数处理错误,而是通过?将其返回给调用者。
如果结果是 Ok,它提取值;如果是 Err,它会直接把错误转换成当前函数的返回类型并 return Err(...)。
我们来看下?的使用规则:
// 使用 ?
let x = foo()?;
// 等价为
let x = match foo() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};? 它也可以用于 Option(在返回 Option 的函数中)——将 None 转为 return None
我们通过?来优化一下,上面倒数运算的例子:
fn get_reciprocal(input: &str) -> Result<f64, String> {
let num = input.parse::<f64>()
.map_err(|_| String::from("解析数字失败"))?; //使用 ? 提前返回或解包
//进行除法运算,直接返回结果 (返回的也是 Result)
safe_divide(1.0, num)
}
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, String> {
if divisor == 0.0 {
return Err(String::from("除数不能为零"));
}
Ok(dividend / divisor)
}
fn main() {
let inputs = vec!["2.0", "0.0", "abc"];
for s in inputs {
println!("{:?} -> {:?}", s, get_reciprocal(s));
}
}我们来看下这段代码,原本的 and_then 被移除,转而我们使用 ? 放在 parse() 调用链的末尾。
如果解析成功,num 会直接获得 f64 类型的值;如果失败,它会立即从函数返回 Err
另外代码流向发生了改变:从函数式的链条传导执行,变成了自上而下执行。这样更加清晰更加直观
到这里有眼尖的朋友,就会问了"为什么map_err还保留",不能用?符号吗
因为换不掉啊,我们必须得明白? 不仅仅是 return Err。它还会自动进行类型转换!但它要求错误类型必须能够通过 From trait 转换
这里input.parse::<f64>() 返回的错误类型是 ParseFloatError,而函数签名要求返回 String。只要错误类型 String 实现了 From<ParseFloatError> trait,? 就能把 ParseFloatError 转换成 String。我们需要记住这个规则,非常的重要
由于标准库没有实现从 ParseFloatError 到 String 的自动转换,所以很可惜, 我们这里需要保留 map_err 来手动转换错误信息
自定义错误类型与错误转换
虽然标准库没有实现ParseFloatError 到 String 的自动转换,但是我们完全可以自定义错误类型啊
我们这里,自定义一个错误类型 MyError,只需再实现From 转换,那我们就可以用 ? 自动把 parse 错误转成 MyError啦
我们这里再优化一下倒数运算的例子:
use std::error::Error;
use std::fmt;
// 自定义错误类型 MyError
#[derive(Debug)]
pub enum MyError {
/// 输入解析成数字失败
ParseError(String),
/// 除数为零
DivideByZero,
}
//为 MyError 实现 Display(错误信息)
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::ParseError(input) => write!(f, "解析数字失败: {}", input),
MyError::DivideByZero => write!(f, "除数不能为零"),
}
}
}
//为 MyError 实现 std::error::Error
impl Error for MyError {}
// From 实现:把 ParseFloatError 自动转成 MyError
// 这样 parse::<f64>()? 就能直接返回 MyError
impl From<std::num::ParseFloatError> for MyError {
fn from(_err: std::num::ParseFloatError) -> Self {
// 我们无法从 ParseFloatError 中获取原始输入
// 所以只能给个通用错误
MyError::ParseError("输入不是有效数字".into())
}
}
//别名
type Result<T> = std::result::Result<T, MyError>;
fn get_reciprocal(input: &str) -> Result<f64> {
// parse()? 自动把 ParseFloatError 转换为 MyError
let num: f64 = input.parse::<f64>()?;
// 使用安全除法
safe_divide(1.0, num)
}
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64> {
if divisor == 0.0 {
return Err(MyError::DivideByZero);
}
Ok(dividend / divisor)
}
fn main() {
let inputs = vec!["2.0", "0.0", "abc"];
for s in inputs {
println!("{:?} -> {:?}", s, get_reciprocal(s));
}
}结果:
"2.0" -> Ok(0.5)
"0.0" -> Err(DivideByZero)
"abc" -> Err(ParseError("输入不是有效数字"))我们这里还实现了Display, 为了让错误信息可读
但感觉这样是不是又繁琐起来了,上面我们的写法,那是非常的标准且严谨,但是我们不得不写impl Display, impl From这样的样板代码

别担心,现在Rust 社区有两个非常牛逼的库thiserror 与 anyhow,能极大地帮我们简化错误处理,目前也事实上成为了Rust错误处理的标准
现代生态系统:thiserror 与 anyhow
thiserror,用于定义结构良好的错误枚举
它通过派生宏帮我们自动生成 Display, From, Error trait 的实现,比较适用于库/Library 开发
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("文件读取失败")] // 自动实现 Display
Io(#[from] std::io::Error), // 自动实现 From<io::Error>
#[error("无效的数据格式")]
InvalidFormat,
#[error("未知错误: {0}")]
Unknown(String),
}anyhow提供动态错误类型anyhow::Error,方便捕获任意错误并附加上下文。适合于应用开发
use anyhow::{Context, Result};
// 注意这里用的是 anyhow::Result ,不是标准库的Result
fn main_logic() -> Result<()> {
let path = "config.json";
let content = std::fs::read_to_string(path)
.with_context(|| format!("尝试读取配置文件 {} 失败", path))?;
println!("Content: {}", content);
Ok(())
}我们再再再来优化下倒数运算的例子,先引入这2个库:
[dependencies]
anyhow = "1.0.98" # 错误处理
thiserror = "2.0.12" # 错误处理接着修改main.rs:
use thiserror::Error;
use anyhow::{Result, Context};
/// 使用 thiserror 定义业务错误
#[derive(Debug, Error)]
pub enum MyError {
/// 输入解析失败,保留原始输入
#[error("解析数字失败: {0}")]
ParseError(String),
/// 除数为零
#[error("除数不能为零")]
DivideByZero,
}
/// 可逆计算
fn get_reciprocal(input: &str) -> Result<f64> {
// parse 时无法从 ParseFloatError 获取具体内容,因此手动处理
let num: f64 = input
.parse::<f64>()
.map_err(|_| MyError::ParseError(input.to_string()))
.context("数字解析失败")?;
safe_divide(1.0, num).context("计算倒数失败")
}
/// 安全除法
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64> {
if divisor == 0.0 {
return Err(MyError::DivideByZero.into());
}
Ok(dividend / divisor)
}
fn main() -> Result<()> {
let inputs = vec!["2.0", "0.0", "abc"];
for s in inputs {
match get_reciprocal(s) {
Ok(v) => println!("{:?} -> Ok({})", s, v),
Err(e) => println!("{:?} -> Err(\"{}\")", s, e),
}
}
Ok(())
}执行结果是:
"2.0" -> Ok(0.5)
"0.0" -> Err("计算倒数失败")
"abc" -> Err("数字解析失败")我们通过 thiserror来自动实现 Display + Error, main函数使用 anyhow::Result<()>,这样我们就可以用用 ? 轻松返回任何错误
可以说,? + anyhow + thiserror 就是目前Rust中错误处理的最佳实践!

关注我的公众号【小牛呼噜噜】,获取更多干货,下期我们再见!
扫码到公众号中,回复关键字:Rust错误处理,即可获取本文完整代码示例和练习项目
作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货 ::: -->