Rust中错误处理机制
Rust 的错误处理是通过Result类型和panic!宏两个层次来完成的:
| 类型 | 含义 | 错误处理 |
|---|---|---|
| 可恢复错误 | 程序逻辑上可以预期并处理,比如文件不存在、网络超时 | 使用 Result<T, E> |
| 不可恢复错误 | 程序出现 bug 或严重问题,无法继续运行 | 使用 panic! 宏 |
| 可选值缺失 | 一个值可能存在(Some(T))或不存在(None),避免空指针问题 | 使用Option<T> |
可恢复错误(Result<T,E>)
Result是一个 泛型枚举,强制调用者处理成功和失败两种情况(是零开销的,由编译器静态保证必须处理):
T:成功时返回的值类型;E:错误时返回的错误类型。
use std::fs::File;
use std::io::Error;fn open_file() -> Result<File, Error> {let file = File::open("hello.txt");match file {Ok(f) => Ok(f),Err(e) => Err(e),}
}
常用方法
Result 提供了大量链式方法,用于灵活地处理成功或失败分支
| 方法 | 说明 |
|---|---|
.is_ok() | 是否是 Ok |
.is_err() | 是否是 Err |
.ok() | 取值,返回Option<T> |
.err() | 取错误,返回Option<E> |
.unwrap() | 成功取值T,否则 panic |
.expect(msg) | 成功取值T,否则 panic 并打印 msg |
.unwrap_or(default) | 失败则返回默认值 |
.unwrap_or_else(f) | 失败时执行函数生成默认值 |
.map(f) | 若是Ok,应用函数F(T) -> U到内部值;否则返回原Err |
.map_err(f) | 对Err执行map |
.and_then(f) | 若是Ok,应用函数F(T) -> Result<U, E>(处理T时可能返回错误)到内部值;否则返回原Err |
.or_else(f) | 对Err执行f |
map与unwrap示例:
fn parse_and_double(vec: Vec<&str>) -> Vec<String> {vec.into_iter().map(|s| {s.parse::<i32>().map(|n| (n * 2).to_string()).unwrap_or_else(|_| s.to_string())}).collect()
}pub fn test_parse_and_double() {let vec = vec!["1", "2", "three", "4"];let doubled = parse_and_double(vec);assert_eq!(doubled, vec!["2", "4", "three", "8"]);
}
unwrap和expect
快速处理错误,失败时会panic:
unwrap():若Result为Ok(v)则返回v;若为Err(e)则触发panic!(适合 “理论上不可能失败” 的场景)。expect(msg):功能与unwrap类似,但可自定义panic!的错误消息(更易调试)。
use std::fs::read_to_string;fn main() {// 若文件不存在,unwrap会paniclet content = read_to_string("test.txt").unwrap(); // 自定义panic消息let content = read_to_string("test.txt").expect("读取test.txt失败");
}
错误传播(? 运算符)
? 是 Rust 中最常用的错误传播工具
- 如果是
Ok(t),提取t; - 如果是
Err(e),提前返回当前函数,并将错误向上传递。
文件读写示例
use std::fs::File;
use std::io::{self, Read};fn read_username() -> Result<String, io::Error> {let mut s = String::new();File::open("username.txt")?.read_to_string(&mut s)?;Ok(s)
}// 等价于
fn read_username() -> Result<String, io::Error> {let mut s = String::new();let f = match File::open("username.txt") {Ok(file) => file,Err(e) => return Err(e),};match f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => return Err(e),}
}
当使用 ? 时,错误类型会自动通过 From trait 转换。这意味着可以组合不同来源的错误,只要能被转换到同一个错误类型。
use std::num::ParseIntError;
use std::io;#[derive(Debug)]
enum MyError {Io(io::Error),Parse(ParseIntError),
}impl From<io::Error> for MyError {fn from(e: io::Error) -> Self { MyError::Io(e) }
}
impl From<ParseIntError> for MyError {fn from(e: ParseIntError) -> Self { MyError::Parse(e) }
}fn read_and_parse() -> Result<i32, MyError> {let s = std::fs::read_to_string("num.txt")?; // io::Error → MyErrorlet num = s.trim().parse::<i32>()?; // ParseIntError → MyErrorOk(num)
}
Option枚举:处理’空值’
Option虽不直接表示 “错误”,但用于处理 “可能没有值” 的场景(避免空指针异常);可认为Option 是 Result 的一个简化版本(错误类型固定为 ()) 。
enum Option<T> {Some(T), // 有值:包含具体值(类型为T)None, // 无值:表示“空”
}
与Result类似,可通过match、?、unwrap等方式处理;Option中的常用方法
| 方法 | 功能 |
|---|---|
is_some()/ is_none() | 检查是否有值 |
unwrap() | 直接取值(若为 None 则 panic) |
unwrap_or(default) | 若为 None返回默认值 |
unwrap_or_else(f) | 若为 None执行函数返回值 |
map(f) | 映射 Some 中的值 |
and_then(f) | 链式操作,f返回 Option |
ok_or(err) | 转换为 Result |
filter(pred) | 条件筛选 |
fn find_user(id: u32) -> Option<String> {if id == 1 {Some("Alice".to_string()) // 找到用户} else {None // 未找到用户}
}fn main() {// 方式1:match匹配match find_user(1) {Some(name) => println!("找到用户:{}", name),None => println!("用户不存在"),}// 方式2:?运算符(需函数返回Option)fn get_username(id: u32) -> Option<String> {let name = find_user(id)?; // 若为None,直接返回NoneSome(name)}// 方式3:unwrap(不存在则panic)let name = find_user(1).unwrap(); // 若为None,panic
}
自定义错误类型
Result中的E 可以是任意类型,因此我们可以:
- 使用
String(简单但不方便匹配) - 使用
Box<dyn Error>(灵活但缺少结构化) - 定义自己的错误类型(推荐方式)
基本要求
自定义错误类型需要满足:
- 必须实现
std::error::Errortrait:使其成为标准错误类型; - 必须实现
std::fmt::Displaytrait:以提供用户友好的错误信息;
use std::fmt;
use std::error::Error;
use std::io;// 自定义错误枚举:包含两种错误类型
#[derive(Debug)] // 必须实现Debug
pub enum MyError {Io(io::Error), // 包装IO错误Parse(String), // 解析错误(携带错误信息)
}// 实现Display trait:定义错误的用户可见信息
impl fmt::Display for MyError {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {match self {MyError::Io(e) => write!(f, "IO操作失败: {}", e),MyError::Parse(msg) => write!(f, "解析失败: {}", msg),}}
}// 现 std::error::Error(让它成为标准错误类型)
impl Error for MyError {// (可选)实现source方法:fn source(&self) -> Option<&(dyn Error + 'static)> {match self {MyError::Io(e) => Some(e), // IO错误的底层是io::ErrorMyError::Parse(_) => None, // 解析错误没有底层错误}}
}pub fn test_my_error() -> Result<(), MyError> {// let e = MyError::Parse("无效的整数".to_string());let e = MyError::Io(io::Error::new(io::ErrorKind::Other, "自定义IO错误"));println!("Error: {}", e);Err(e)
}
实现 From
为错误类型实现 From,这样就能使用 ? 运算符自动转换错误
impl From<std::io::Error> for MyError {fn from(err: std::io::Error) -> Self {MyError::Io(err)}
}impl From<std::num::ParseIntError> for MyError {fn from(err: std::num::ParseIntError) -> Self {MyError::Parse(err)}
}fn read_number_from_file() -> Result<i32, MyError> {let content = std::fs::read_to_string("data.txt")?; // 自动转成 MyError::Iolet num = content.trim().parse::<i32>()?; // 自动转成 MyError::ParseOk(num)
}
三方库简化错误定义
手写 Display、From 很繁琐,Rust 社区提供了两个常用库。
thiserror(#[derive(Error)] )-定义结构化错误 :
#[derive(Error, Debug)]:自动生成Error和Debugtrait 的实现。#[error("消息模板")]:定义Displaytrait 的输出格式。#[from]:自动实现From<T>trait(T是字段类型),允许通过?运算符将T类型的错误自动转换为当前自定义错误。
use thiserror::Error;#[derive(Debug, Error)]
pub enum MyError {#[error("IO错误: {0}")]Io(#[from] std::io::Error),#[error("解析错误: {0}")]Parse(#[from] std::num::ParseIntError),#[error("未找到数据")]NotFound,
}
anyhow-简化上层错误处理:
anyhow::Result<T>实际是Result<T, anyhow::Error>context方法会将原始错误,并添加额外描述,便于追踪错误发生的场景。
use anyhow::{Result, Context};// 函数返回anyhow::Error,可容纳任何实现Error trait的类型
pub fn test_anyhow() -> Result<()> {test_my_error().context("test failed")?; // 为错误添加上下文Ok(())
}
不可恢复错误(panic!)
当程序遇到不可恢复的错误(如违反内存安全、逻辑断言失败)时,使用panic!宏触发程序终止。默认会导致程序终止,但在某些场景下(如隔离第三方库的崩溃、实现容错机制),我们需要’安全捕获’panic 以避免程序整体崩溃。
fn divide(a: i32, b: i32) -> i32 {if b == 0 {panic!("除数不能为0"); // 逻辑错误:不允许除0}a / b
}
当 panic! 被触发时,Rust 默认会执行 “栈展开”(可在Cargo.toml中添加panic = "abort" 直接终止,不进行展开):
- 从
panic!发生的位置开始,沿着调用栈向上回溯。 - 依次调用每个栈帧中局部变量的析构函数(清理资源,如关闭文件、释放锁等)。
- 最终打印错误信息(包含
panic!的位置和消息)并终止程序。
这种行为保证了 “资源安全”—— 即使程序崩溃,也不会泄露资源。
catch_unwind
atch_unwind 接收一个闭包作为参数,返回 Result<(), Box<dyn Any + Send>>:
- 若闭包执行中未发生
panic,返回Ok(())。 - 若发生
panic,返回Err(cause),其中cause是panic!传递的值(包装为Box<dyn Any + Send>)。
use std::panic;fn main() {// 捕获闭包中的paniclet result = panic::catch_unwind(|| {println!("执行可能 panic 的代码");panic!("发生错误!"); // 触发panic});// 处理捕获结果match result {Ok(_) => println!("未发生 panic"),Err(cause) => {// 将cause转换为具体类型(通常是&str或String)if let Some(msg) = cause.downcast_ref::<&str>() {println!("捕获到 panic:{}", msg);} else {println!("捕获到未知 panic");}}}// 程序继续执行(不会终止)println!("捕获 panic 后,程序继续运行");
}
catch_unwind 并非万能,存在以下关键限制:
- 仅能捕获 “栈展开” 的
panic:若程序配置为panic = "abort",catch_unwind无法捕获(因为不会触发栈展开)。 - 无法捕获所有终止情况:如调用
std::process::abort()、操作系统信号(如SIGKILL)导致的终止,catch_unwind无效。 - 闭包需满足
Send约束:catch_unwind的闭包参数要求FnOnce() + Send,这意味着闭包中不能包含非Send类型(如Rc、RefCell等线程不安全类型)。 - 程序状态可能不稳定:
panic发生时,栈展开会清理局部资源,但全局状态、共享资源可能处于不一致状态(如部分更新的缓存)。捕获panic后需谨慎处理,避免依赖不稳定状态。
资源与不变式
当线程在持有 Mutex 时 panic,该 Mutex 会被标记为“poisoned”(污染)。随后 lock() 会返回 PoisonError,表示需要小心处理共享状态可能处于不一致状态。
use std::sync::Mutex;let m = Mutex::new(0);
// 线程 panic 造成 poison 的示例请参见文档
match m.lock() {Ok(mut guard) => *guard += 1,Err(poisoned) => {// 取回锁仍然可用的数据,但知道它可能不一致let mut guard = poisoned.into_inner();*guard += 1;}
}
有效载荷payload
panic!() 可以携带 &'static str 或 String 等任意 Any + Send 类型作为 payload。捕获后可以尝试 downcast_ref 来读取消息:
use std::panic;if let Err(payload) = panic::catch_unwind(|| panic!("oh no")) {if let Some(s) = payload.downcast_ref::<&str>() {println!("&str payload: {}", s);} else if let Some(s) = payload.downcast_ref::<String>() {println!("String payload: {}", s);} else {println!("未知 payload 类型");}
}
