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"]);
}

unwrapexpect

快速处理错误,失败时会panic:

  • unwrap():若ResultOk(v)则返回v;若为Err(e)则触发panic!(适合 “理论上不可能失败” 的场景)。
  • expect(msg):功能与unwrap类似,但可自定义panic!的错误消息(更易调试)。
use std::fs::read_to_string;

fn main() {
    // 若文件不存在,unwrap会panic
    let 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 → MyError
    let num = s.trim().parse::<i32>()?;          // ParseIntError → MyError
    Ok(num)
}

Option枚举:处理’空值’

Option虽不直接表示 “错误”,但用于处理 “可能没有值” 的场景(避免空指针异常);可认为OptionResult 的一个简化版本(错误类型固定为 ()) 。

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,直接返回None
        Some(name)
    }

    // 方式3:unwrap(不存在则panic)
    let name = find_user(1).unwrap(); // 若为None,panic
}

自定义错误类型

Result中的E 可以是任意类型,因此我们可以:

  • 使用 String(简单但不方便匹配)
  • 使用 Box<dyn Error>(灵活但缺少结构化)
  • 定义自己的错误类型(推荐方式)

基本要求

自定义错误类型需要满足:

  • 必须实现std::error::Error trait:使其成为标准错误类型;
  • 必须实现std::fmt::Display trait:以提供用户友好的错误信息;
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::Error
            MyError::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::Io
    let num = content.trim().parse::<i32>()?;           // 自动转成 MyError::Parse
    Ok(num)
}

三方库简化错误定义

手写 DisplayFrom 很繁琐,Rust 社区提供了两个常用库。

thiserror#[derive(Error)] )-定义结构化错误 :

  • #[derive(Error, Debug)]:自动生成 ErrorDebug trait 的实现。
  • #[error("消息模板")]:定义 Display trait 的输出格式。
  • #[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),其中 causepanic! 传递的值(包装为 Box<dyn Any + Send>)。
use std::panic;

fn main() {
    // 捕获闭包中的panic
    let 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 类型(如 RcRefCell 等线程不安全类型)。
  • 程序状态可能不稳定: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 strString 等任意 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 类型");
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值