首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust专项——错误处理实战:Result、`?`、thiserror 与 anyhow

Rust专项——错误处理实战:Result、`?`、thiserror 与 anyhow

作者头像
红目香薰
发布2025-12-16 16:31:02
发布2025-12-16 16:31:02
1440
举报
文章被收录于专栏:CSDNToQQCodeCSDNToQQCode

本节系统梳理 Rust 错误处理:从标准 Result<T, E>? 运算符,到库级自定义错误(thiserror),再到应用层快速落地(anyhow)。目标是:接口清晰、边界合理、信息友好、可追踪可定位


1. 基础:Result 与 ? 运算符

代码语言:javascript
复制
fn read_num(path: &str) -> Result<i32, std::io::Error> {
    let s = std::fs::read_to_string(path)?;      // `?` 等价于早返回 Err
    Ok(s.trim().parse::<i32>().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?)
}
  • ?:若为 Err(e) 则立刻返回 Err(e);若为 Ok(v) 则提取 v
  • map_err:把下游错误转换为上游需要的错误类型。

2. 自定义错误:thiserror

库/模块对外公开精确可枚举的错误类型,适合做库边界

Cargo.toml:

代码语言:javascript
复制
[dependencies]
thiserror = "1"

定义:

代码语言:javascript
复制
use thiserror::Error;

#[derive(Debug, Error)]
pub enum RepoError {
    #[error("I/O失败: {0}")] Io(#[from] std::io::Error),         // From 自动生成
    #[error("解析失败: {0}")] ParseInt(#[from] std::num::ParseIntError),
    #[error("键不存在: {0}")] MissingKey(String),
}

pub fn load_num(path: &str) -> Result<i32, RepoError> {
    let raw = std::fs::read_to_string(path)?;     // 自动转成 RepoError::Io
    let n: i32 = raw.trim().parse()?;             // 自动转成 RepoError::ParseInt
    Ok(n)
}

要点:

  • #[from] 派生 From<下游错误>? 即可自动转换;
  • 错误信息可本地化/结构化,便于前端提示与日志检索。

3. 应用层快速落地:anyhow

适合 CLI/服务应用的顶层错误处理:无需为所有错误建枚举,快速聚合并携带上下文。

Cargo.toml:

代码语言:javascript
复制
[dependencies]
anyhow = "1"

使用:

代码语言:javascript
复制
use anyhow::{Context, Result};

fn run(path: &str) -> Result<()> {
    let s = std::fs::read_to_string(path)
        .with_context(|| format!("读取文件失败: {}", path))?;  // 追加上下文
    let n: i32 = s.trim().parse()
        .context("内容不是有效整数")?;                      // 简明描述
    println!("num={}", n);
    Ok(())
}
  • Context/with_context 可级联丰富“错误链”,定位更快。
  • anyhow::Result<T> = Result<T, anyhow::Error>:统一错误盒子,适合应用入口。

4. 选择策略:库 vs 应用

场景

推荐

理由

库(对外 API)

自定义枚举 + thiserror

错误可枚举、可匹配、语义稳定

应用边界(main/CLI/服务顶层)

anyhow

快速传递、可堆叠上下文,日志友好

库内/应用内子模块

Result<T, E> + ?

清晰、无样板,按需 map_err


5. 错误边界与转换

  • 边界设计:库层抛自定义错误;跨边界时再转 anyhow::Error 统一日志;
  • 从专用到通用impl From<MyErr> for anyhow::Erroranyhow已内置大部分 From)。
代码语言:javascript
复制
fn service_handle(path: &str) -> anyhow::Result<()> {
    let n = load_num(path)?;               // RepoError 自动转 anyhow::Error
    println!("n={}", n);
    Ok(())
}

6. map / map_err / and_then:组合函数式风格

代码语言:javascript
复制
fn parse_list(s: &str) -> Result<Vec<i32>, std::num::ParseIntError> {
    s.split(',').map(|t| t.trim().parse()).collect()
}

fn io_then_parse(path: &str) -> anyhow::Result<Vec<i32>> {
    let raw = std::fs::read_to_string(path)?;
    Ok(parse_list(&raw)?)
}

7. 单元测试与断言

代码语言:javascript
复制
#[test]
fn test_parse_err() {
    let err = "a,b".trim().parse::<i32>().unwrap_err();
    assert_eq!(err.kind(), std::num::IntErrorKind::InvalidDigit);
}

8. 常见坑位与修复

  • Droppanic! → 可能触发二次 panic;改为日志记录。
  • 滥用 unwrap()/expect() → 生产环境尽量改为 ? 或显式处理。
  • 一味使用 anyhow 做库错误 → 失去可匹配性与稳定 API;库层用 thiserror
  • 丢失上下文 → 用 with_context() 堆叠关键信息:参数、路径、远端地址等。
  • 错误枚举过度膨胀 → 按边界分层,复用 #[from]source

9. 实战:小型拷贝工具(带上下文链)

先创建个文件,看好项目根目录。

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
use anyhow::{Context, Result};
use std::io::{Read, Write};

fn copy_file(from: &str, to: &str) -> Result<()> {
    // 打开源文件,添加上下文信息
    let mut input = std::fs::File::open(from)
        .with_context(|| format!("打开源文件失败: {}", from))?;
    
    // 创建目标文件,添加上下文信息
    let mut output = std::fs::File::create(to)
        .with_context(|| format!("创建目标文件失败: {}", to))?;

    // 读取源文件内容到缓冲区
    let mut buf = Vec::new();
    input.read_to_end(&mut buf)
        .context("读取源文件内容失败")?;
    
    // 将缓冲区内容写入目标文件
    output.write_all(&buf)
        .context("写入目标文件内容失败")?;
    
    Ok(())
}

// 测试函数(可选,用于验证功能)
fn main() -> Result<()> {
    // 示例:复制当前目录下的 "source.txt" 到 "dest.txt"
    copy_file("source.txt", "dest.txt")
        .with_context(|| "文件复制过程出错")?;
    println!("文件复制成功!");
    Ok(())
}
在这里插入图片描述
在这里插入图片描述

10. 练习

  1. 为一个 JSON 解析库封装错误:枚举包含 Io/Serde/Schema 三类,派生 thiserror::Error,实现 From
  2. 写一个命令 sum <path>:逐行读取整数求和,用 anyhow::Context 给每一步加来源信息。
  3. 思考:库层为什么不直接用 anyhow::Error 暴露?如何在不破坏稳定 API 的前提下追加新错误场景?

小结:Rust 错误处理的精髓在于“分层设计”:库层自定义可匹配错误,应用层聚合并堆叠上下文;? 串联流程,thiserror/anyhow 减少样板代码,让错误既易查也易用。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 基础:Result 与 ? 运算符
  • 2. 自定义错误:thiserror
  • 3. 应用层快速落地:anyhow
  • 4. 选择策略:库 vs 应用
  • 5. 错误边界与转换
  • 6. map / map_err / and_then:组合函数式风格
  • 7. 单元测试与断言
  • 8. 常见坑位与修复
  • 9. 实战:小型拷贝工具(带上下文链)
  • 10. 练习
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档