【Rust入门练中学】第8课:错误处理

目标:掌握 panic!、Result<T, E>、? 运算符、常用辅助方法、自定义错误类型,理解两类错误的边界、? 的自动转换机制,以及实际项目中的错误处理策略。


📚 文章目录

  • 一、从错误的本质说起
  • 二、panic!:不可恢复错误
    • [1. 基本用法](#1. 基本用法)
    • [2. 什么时候用 panic!](#2. 什么时候用 panic!)
    • [3. panic! 的两种展开方式](#3. panic! 的两种展开方式)
    • [4. panic! 会释放资源](#4. panic! 会释放资源)
  • [三、Result<T, E>:可恢复错误](#三、Result<T, E>:可恢复错误)
    • [1. 定义](#1. 定义)
    • [2. 基本用法](#2. 基本用法)
    • [3. 错误的类型](#3. 错误的类型)
    • [4. 处理错误的不同策略](#4. 处理错误的不同策略)
  • [四、? 运算符:错误传播的利器](#四、? 运算符:错误传播的利器)
    • [1. 基本用法](#1. 基本用法)
    • [2. ? 做了三件事](#2. ? 做了三件事)
    • [3. ? 与 Option](#3. ? 与 Option)
    • [4. main 返回 Result](#4. main 返回 Result)
  • 五、常用辅助方法
    • [1. unwrap 和 expect](#1. unwrap 和 expect)
    • [2. unwrap_or 系列](#2. unwrap_or 系列)
    • [3. 判断方法](#3. 判断方法)
    • [4. 转换方法](#4. 转换方法)
    • [5. ok() 和 err()](#5. ok() 和 err())
    • [6. ok_or 和 ok_or_else](#6. ok_or 和 ok_or_else)
  • 六、选择正确的错误处理方式
    • [1. 判断标准](#1. 判断标准)
    • [2. 场景对照](#2. 场景对照)
    • [3. unwrap 的使用原则](#3. unwrap 的使用原则)
  • 七、自定义错误类型
    • [1. 为什么需要](#1. 为什么需要)
    • [2. Box<dyn Error>](#2. Box)
    • [3. 自定义错误枚举](#3. 自定义错误枚举)
    • [4. thiserror 和 anyhow](#4. thiserror 和 anyhow)
  • 八、多元化练习题(含答案与解读)
  • 九、知识点总结(第8课)
  • 十、下一课预告

下面是本文的知识地图,帮助你快速定位各类错误处理工具:
#mermaid-svg-6V4wyG8ca6hsI6bd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6V4wyG8ca6hsI6bd .error-icon{fill:#552222;}#mermaid-svg-6V4wyG8ca6hsI6bd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6V4wyG8ca6hsI6bd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6V4wyG8ca6hsI6bd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6V4wyG8ca6hsI6bd .marker.cross{stroke:#333333;}#mermaid-svg-6V4wyG8ca6hsI6bd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6V4wyG8ca6hsI6bd p{margin:0;}#mermaid-svg-6V4wyG8ca6hsI6bd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster-label text{fill:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster-label span{color:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster-label span p{background-color:transparent;}#mermaid-svg-6V4wyG8ca6hsI6bd .label text,#mermaid-svg-6V4wyG8ca6hsI6bd span{fill:#333;color:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd .node rect,#mermaid-svg-6V4wyG8ca6hsI6bd .node circle,#mermaid-svg-6V4wyG8ca6hsI6bd .node ellipse,#mermaid-svg-6V4wyG8ca6hsI6bd .node polygon,#mermaid-svg-6V4wyG8ca6hsI6bd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6V4wyG8ca6hsI6bd .rough-node .label text,#mermaid-svg-6V4wyG8ca6hsI6bd .node .label text,#mermaid-svg-6V4wyG8ca6hsI6bd .image-shape .label,#mermaid-svg-6V4wyG8ca6hsI6bd .icon-shape .label{text-anchor:middle;}#mermaid-svg-6V4wyG8ca6hsI6bd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6V4wyG8ca6hsI6bd .rough-node .label,#mermaid-svg-6V4wyG8ca6hsI6bd .node .label,#mermaid-svg-6V4wyG8ca6hsI6bd .image-shape .label,#mermaid-svg-6V4wyG8ca6hsI6bd .icon-shape .label{text-align:center;}#mermaid-svg-6V4wyG8ca6hsI6bd .node.clickable{cursor:pointer;}#mermaid-svg-6V4wyG8ca6hsI6bd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6V4wyG8ca6hsI6bd .arrowheadPath{fill:#333333;}#mermaid-svg-6V4wyG8ca6hsI6bd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6V4wyG8ca6hsI6bd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6V4wyG8ca6hsI6bd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6V4wyG8ca6hsI6bd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6V4wyG8ca6hsI6bd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6V4wyG8ca6hsI6bd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster text{fill:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd .cluster span{color:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6V4wyG8ca6hsI6bd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6V4wyG8ca6hsI6bd rect.text{fill:none;stroke-width:0;}#mermaid-svg-6V4wyG8ca6hsI6bd .icon-shape,#mermaid-svg-6V4wyG8ca6hsI6bd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6V4wyG8ca6hsI6bd .icon-shape p,#mermaid-svg-6V4wyG8ca6hsI6bd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6V4wyG8ca6hsI6bd .icon-shape .label rect,#mermaid-svg-6V4wyG8ca6hsI6bd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6V4wyG8ca6hsI6bd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6V4wyG8ca6hsI6bd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6V4wyG8ca6hsI6bd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不可恢复
可恢复
程序出错
错误类型
panic!
Result<T, E>
栈展开 unwinding
终止 abort
处理策略
立即处理 match
给默认值 unwrap_or
向上传播 ?
崩溃 unwrap/expect
辅助方法
map / map_err
ok / err
ok_or / ok_or_else
自定义错误
Box<dyn Error>
错误枚举 + From
thiserror / anyhow

一、从错误的本质说起

程序出错是常态。文件可能不存在、网络可能断开、用户可能输入非法数据、代码可能有 bug。关键不是"避免错误",而是如何优雅地应对错误。

Rust 把错误分成两类,对应两种处理方式:

不可恢复错误 :程序有 bug,继续跑没意义。比如数组越界、断言失败、unwrap 到 None。用 panic! 直接崩溃。

可恢复错误 :错误是预期内的,调用方可能能处理。比如文件不存在、解析失败、网络超时。用 Result<T, E> 让调用方决定。

其他语言的处理方式对比:

异常 (Java、Python、C++):错误可以"悄悄"传播,调用方不写 try 就崩溃。编译器不强制你处理,容易漏。

错误码 (C、Go):函数返回 -1 或 null,调用方容易忘记检查。类型系统不帮你。

Rust 的方案 :把错误编码进类型 。Result<T, E> 强制你处理。这是"让非法状态无法表示"思路的延续------错误也是状态,就该用类型表达。

一句话总结:其他语言把错误当"意外",Rust 把错误当"数据"。既然是数据,就用类型系统管起来。

心智模型 :把 Result<T, E> 想象成一张快递单。Ok(v) 是"包裹送达,里面是 v",Err(e) 是"包裹丢失,原因是 e"。收件人(调用方)必须签收或拒收,不能假装没看见------编译器盯着你。


二、panic!:不可恢复错误

1. 基本用法

rust 复制代码
fn main() {
    panic!("出大事了");
}

运行输出(Rust 1.72+ 格式):

复制代码
thread 'main' panicked at src/main.rs:2:5:
出大事了
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic! 会打印错误信息、展开栈、退出程序。

2. 什么时候用 panic!

代码有 bug:逻辑上不可能到达的分支。

rust 复制代码
match direction {
    Direction::Up => ...,
    Direction::Down => ...,
    Direction::Left => ...,
    Direction::Right => ...,
    // 如果加了新变体但忘了处理,编译器会报错
}

违反契约 :索引越界、unwrap 到 None、整数除零。

rust 复制代码
let v = vec![1, 2, 3];
let x = v[10];   // panic: index out of bounds

原型和测试:快速失败,看到错误就停。

不可恢复的系统错误:内存分配失败、栈溢出。

判断标准 :panic! 表示"程序员的假设被打破了"。如果这个错误说明代码本身有问题,就该 panic!;如果只是运行时环境不配合,就该 Result。

3. panic! 的两种展开方式

Rust 默认用栈展开(unwinding):panic 时逐层退出栈帧,调用每个栈帧的析构函数,释放资源。

另一种是终止(abort):panic 时直接终止程序,不清理。产物更小,性能更好。

toml 复制代码
# Cargo.toml
[profile.release]
panic = "abort"

栈展开 更安全,能保证资源释放;终止更快,适合嵌入式或对二进制大小敏感的场景。默认是展开。

下面是 panic! 两种展开方式的对比流程图:
#mermaid-svg-gJh42xLDzi2VDH0c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gJh42xLDzi2VDH0c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gJh42xLDzi2VDH0c .error-icon{fill:#552222;}#mermaid-svg-gJh42xLDzi2VDH0c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gJh42xLDzi2VDH0c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gJh42xLDzi2VDH0c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gJh42xLDzi2VDH0c .marker.cross{stroke:#333333;}#mermaid-svg-gJh42xLDzi2VDH0c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gJh42xLDzi2VDH0c p{margin:0;}#mermaid-svg-gJh42xLDzi2VDH0c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gJh42xLDzi2VDH0c .cluster-label text{fill:#333;}#mermaid-svg-gJh42xLDzi2VDH0c .cluster-label span{color:#333;}#mermaid-svg-gJh42xLDzi2VDH0c .cluster-label span p{background-color:transparent;}#mermaid-svg-gJh42xLDzi2VDH0c .label text,#mermaid-svg-gJh42xLDzi2VDH0c span{fill:#333;color:#333;}#mermaid-svg-gJh42xLDzi2VDH0c .node rect,#mermaid-svg-gJh42xLDzi2VDH0c .node circle,#mermaid-svg-gJh42xLDzi2VDH0c .node ellipse,#mermaid-svg-gJh42xLDzi2VDH0c .node polygon,#mermaid-svg-gJh42xLDzi2VDH0c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gJh42xLDzi2VDH0c .rough-node .label text,#mermaid-svg-gJh42xLDzi2VDH0c .node .label text,#mermaid-svg-gJh42xLDzi2VDH0c .image-shape .label,#mermaid-svg-gJh42xLDzi2VDH0c .icon-shape .label{text-anchor:middle;}#mermaid-svg-gJh42xLDzi2VDH0c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gJh42xLDzi2VDH0c .rough-node .label,#mermaid-svg-gJh42xLDzi2VDH0c .node .label,#mermaid-svg-gJh42xLDzi2VDH0c .image-shape .label,#mermaid-svg-gJh42xLDzi2VDH0c .icon-shape .label{text-align:center;}#mermaid-svg-gJh42xLDzi2VDH0c .node.clickable{cursor:pointer;}#mermaid-svg-gJh42xLDzi2VDH0c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gJh42xLDzi2VDH0c .arrowheadPath{fill:#333333;}#mermaid-svg-gJh42xLDzi2VDH0c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gJh42xLDzi2VDH0c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gJh42xLDzi2VDH0c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gJh42xLDzi2VDH0c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gJh42xLDzi2VDH0c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gJh42xLDzi2VDH0c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gJh42xLDzi2VDH0c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gJh42xLDzi2VDH0c .cluster text{fill:#333;}#mermaid-svg-gJh42xLDzi2VDH0c .cluster span{color:#333;}#mermaid-svg-gJh42xLDzi2VDH0c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gJh42xLDzi2VDH0c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gJh42xLDzi2VDH0c rect.text{fill:none;stroke-width:0;}#mermaid-svg-gJh42xLDzi2VDH0c .icon-shape,#mermaid-svg-gJh42xLDzi2VDH0c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gJh42xLDzi2VDH0c .icon-shape p,#mermaid-svg-gJh42xLDzi2VDH0c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gJh42xLDzi2VDH0c .icon-shape .label rect,#mermaid-svg-gJh42xLDzi2VDH0c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gJh42xLDzi2VDH0c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gJh42xLDzi2VDH0c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gJh42xLDzi2VDH0c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 默认
配置
panic! 触发
展开方式
栈展开 unwinding
终止 abort
逐层退出栈帧
调用析构函数
释放资源
安全退出
直接终止程序
不清理资源
产物更小
性能更好
适合:通用场景
适合:嵌入式/体积敏感

4. panic! 会释放资源

即使 panic,Rust 也会调用析构函数,释放内存、关闭文件:

rust 复制代码
fn main() {
    let _file = std::fs::File::open("data.txt").unwrap();
    panic!("崩溃");
    // _file 会被 drop,文件句柄被关闭
}

这是 Rust 的 RAII(Resource Acquisition Is Initialization)机制。C 语言 abort() 不释放资源,Rust 的展开会。

为什么这很重要? 因为它意味着 panic 不会导致资源泄漏。即使程序崩溃,文件句柄、网络连接、锁都会被正确释放。这是 Rust "崩溃也安全"的一部分。


三、Result<T, E>:可恢复错误

1. 定义

rust 复制代码
enum Result<T, E> {
    Ok(T),    // 成功,装值
    Err(E),   // 失败,装错误
}

Ok 和 Err 已预导入,直接用。

关键理解 :Result 就是一个枚举,和第7课学的 Option 一样。Option 表达"有或没有",Result 表达"成功或失败"。掌握了枚举和 match,就掌握了 Result 的一半。

2. 基本用法

rust 复制代码
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => println!("打开成功: {:?}", file),
        Err(e) => println!("打开失败: {}", e),
    }
}

File::open 返回 Result<File, io::Error>。成功时 Ok(File),失败时 Err(io::Error)。

3. 错误的类型

标准库的错误类型多种多样:

  • std::io::Error:文件、网络、IO
  • std::num::ParseIntError:整数解析
  • std::num::ParseFloatError:浮点解析
  • std::string::FromUtf8Error:UTF-8 转换
  • std::env::VarError:环境变量

每个都实现了 std::error::Error trait,有统一的接口。这意味着它们都能被 Box<dyn Error> 容纳,都能用 ? 传播,都能链式追溯根源。

4. 处理错误的不同策略

策略一:立即处理

rust 复制代码
let f = match File::open("hello.txt") {
    Ok(f) => f,
    Err(e) => {
        eprintln!("无法打开: {}", e);
        return;
    }
};

策略二:给默认值

rust 复制代码
let content = std::fs::read_to_string("config.txt")
    .unwrap_or_else(|_| String::from("默认配置"));

策略三:向上传播

rust 复制代码
fn read_config() -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string("config.txt")?;
    Ok(content)
}

策略四:崩溃

rust 复制代码
let f = File::open("config.txt").expect("配置文件必须存在");

选择哪种取决于:这个错误,调用方能做什么?能做点什么,就传播;做不了什么,就崩溃。


下面是四种错误处理策略的选择流程图:
#mermaid-svg-0MjMtTG3mGmpuRpp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0MjMtTG3mGmpuRpp .error-icon{fill:#552222;}#mermaid-svg-0MjMtTG3mGmpuRpp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0MjMtTG3mGmpuRpp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0MjMtTG3mGmpuRpp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0MjMtTG3mGmpuRpp .marker.cross{stroke:#333333;}#mermaid-svg-0MjMtTG3mGmpuRpp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0MjMtTG3mGmpuRpp p{margin:0;}#mermaid-svg-0MjMtTG3mGmpuRpp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster-label text{fill:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster-label span{color:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster-label span p{background-color:transparent;}#mermaid-svg-0MjMtTG3mGmpuRpp .label text,#mermaid-svg-0MjMtTG3mGmpuRpp span{fill:#333;color:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp .node rect,#mermaid-svg-0MjMtTG3mGmpuRpp .node circle,#mermaid-svg-0MjMtTG3mGmpuRpp .node ellipse,#mermaid-svg-0MjMtTG3mGmpuRpp .node polygon,#mermaid-svg-0MjMtTG3mGmpuRpp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0MjMtTG3mGmpuRpp .rough-node .label text,#mermaid-svg-0MjMtTG3mGmpuRpp .node .label text,#mermaid-svg-0MjMtTG3mGmpuRpp .image-shape .label,#mermaid-svg-0MjMtTG3mGmpuRpp .icon-shape .label{text-anchor:middle;}#mermaid-svg-0MjMtTG3mGmpuRpp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0MjMtTG3mGmpuRpp .rough-node .label,#mermaid-svg-0MjMtTG3mGmpuRpp .node .label,#mermaid-svg-0MjMtTG3mGmpuRpp .image-shape .label,#mermaid-svg-0MjMtTG3mGmpuRpp .icon-shape .label{text-align:center;}#mermaid-svg-0MjMtTG3mGmpuRpp .node.clickable{cursor:pointer;}#mermaid-svg-0MjMtTG3mGmpuRpp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0MjMtTG3mGmpuRpp .arrowheadPath{fill:#333333;}#mermaid-svg-0MjMtTG3mGmpuRpp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0MjMtTG3mGmpuRpp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0MjMtTG3mGmpuRpp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0MjMtTG3mGmpuRpp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0MjMtTG3mGmpuRpp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0MjMtTG3mGmpuRpp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster text{fill:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp .cluster span{color:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0MjMtTG3mGmpuRpp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0MjMtTG3mGmpuRpp rect.text{fill:none;stroke-width:0;}#mermaid-svg-0MjMtTG3mGmpuRpp .icon-shape,#mermaid-svg-0MjMtTG3mGmpuRpp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0MjMtTG3mGmpuRpp .icon-shape p,#mermaid-svg-0MjMtTG3mGmpuRpp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0MjMtTG3mGmpuRpp .icon-shape .label rect,#mermaid-svg-0MjMtTG3mGmpuRpp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0MjMtTG3mGmpuRpp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0MjMtTG3mGmpuRpp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0MjMtTG3mGmpuRpp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 能立即处理
有合理默认值
交给上层处理
无法处理
遇到 Result<T, E>
调用方能做什么?
策略一:立即处理
match 匹配
记录日志 / 提示用户
策略二:给默认值
unwrap_or
unwrap_or_else
策略三:向上传播
返回 Result
使用 ? 运算符
策略四:崩溃
unwrap / expect
panic!

四、? 运算符:错误传播的利器

1. 基本用法

? 用在返回 Result 的函数里,遇到 Err 就提前返回,遇到 Ok 就解包继续。

rust 复制代码
use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut f = File::open(path)?;      // 失败就返回 Err
    let mut s = String::new();
    f.read_to_string(&mut s)?;          // 失败就返回 Err
    Ok(s)
}

注意 :read_to_string 是 std::io::Read trait 的方法,必须 use std::io::Read; 才能用。这是新手最常踩的坑之一------编译器报错"no method named read_to_string",其实只是忘了导入 trait。

对比不用 ? 的版本:

rust 复制代码
fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut f = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => {}
        Err(e) => return Err(e),
    }
    Ok(s)
}

? 让错误传播变得极简。可读性和直接写正常逻辑几乎一样。

2. ? 做了三件事

第一,成功解包 :Ok(v) 变成 v,继续执行。

第二,失败返回 :Err(e) 变成 return Err(e),提前退出。

第三,自动类型转换 :如果错误类型不匹配,自动调用 From::from。

rust 复制代码
use std::fs::File;
use std::io::Read;

fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let mut f = File::open(path)?;              // io::Error
    let mut s = String::new();
    f.read_to_string(&mut s)?;                   // io::Error
    let n = s.trim().parse::<i32>()?;            // ParseIntError
    Ok(n)
}

? 把 io::Error 和 ParseIntError 都转成 Box<dyn Error>。前提是目标错误类型实现了 From<源错误类型>。

关于 Box<dyn Error> 的约束 :Box<dyn Error> 等价于 Box<dyn Error + 'static>,只能装 'static 的错误类型。标准库的 io::Error、ParseIntError 都是 'static,所以能转。如果自定义错误类型带生命周期(比如 MyError<'a> 含有 &'a str),就不能转进 Box<dyn Error>,需要写成 Box<dyn Error + 'a> 并显式标注生命周期。

展开形式 :? 大致展开成:

rust 复制代码
match expr {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

下面是 ? 运算符的工作流程示意图:
#mermaid-svg-I7V0XPqNaop5M0gw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I7V0XPqNaop5M0gw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I7V0XPqNaop5M0gw .error-icon{fill:#552222;}#mermaid-svg-I7V0XPqNaop5M0gw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I7V0XPqNaop5M0gw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I7V0XPqNaop5M0gw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I7V0XPqNaop5M0gw .marker.cross{stroke:#333333;}#mermaid-svg-I7V0XPqNaop5M0gw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I7V0XPqNaop5M0gw p{margin:0;}#mermaid-svg-I7V0XPqNaop5M0gw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I7V0XPqNaop5M0gw .cluster-label text{fill:#333;}#mermaid-svg-I7V0XPqNaop5M0gw .cluster-label span{color:#333;}#mermaid-svg-I7V0XPqNaop5M0gw .cluster-label span p{background-color:transparent;}#mermaid-svg-I7V0XPqNaop5M0gw .label text,#mermaid-svg-I7V0XPqNaop5M0gw span{fill:#333;color:#333;}#mermaid-svg-I7V0XPqNaop5M0gw .node rect,#mermaid-svg-I7V0XPqNaop5M0gw .node circle,#mermaid-svg-I7V0XPqNaop5M0gw .node ellipse,#mermaid-svg-I7V0XPqNaop5M0gw .node polygon,#mermaid-svg-I7V0XPqNaop5M0gw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I7V0XPqNaop5M0gw .rough-node .label text,#mermaid-svg-I7V0XPqNaop5M0gw .node .label text,#mermaid-svg-I7V0XPqNaop5M0gw .image-shape .label,#mermaid-svg-I7V0XPqNaop5M0gw .icon-shape .label{text-anchor:middle;}#mermaid-svg-I7V0XPqNaop5M0gw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I7V0XPqNaop5M0gw .rough-node .label,#mermaid-svg-I7V0XPqNaop5M0gw .node .label,#mermaid-svg-I7V0XPqNaop5M0gw .image-shape .label,#mermaid-svg-I7V0XPqNaop5M0gw .icon-shape .label{text-align:center;}#mermaid-svg-I7V0XPqNaop5M0gw .node.clickable{cursor:pointer;}#mermaid-svg-I7V0XPqNaop5M0gw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I7V0XPqNaop5M0gw .arrowheadPath{fill:#333333;}#mermaid-svg-I7V0XPqNaop5M0gw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I7V0XPqNaop5M0gw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I7V0XPqNaop5M0gw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I7V0XPqNaop5M0gw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I7V0XPqNaop5M0gw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I7V0XPqNaop5M0gw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I7V0XPqNaop5M0gw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I7V0XPqNaop5M0gw .cluster text{fill:#333;}#mermaid-svg-I7V0XPqNaop5M0gw .cluster span{color:#333;}#mermaid-svg-I7V0XPqNaop5M0gw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I7V0XPqNaop5M0gw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I7V0XPqNaop5M0gw rect.text{fill:none;stroke-width:0;}#mermaid-svg-I7V0XPqNaop5M0gw .icon-shape,#mermaid-svg-I7V0XPqNaop5M0gw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I7V0XPqNaop5M0gw .icon-shape p,#mermaid-svg-I7V0XPqNaop5M0gw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I7V0XPqNaop5M0gw .icon-shape .label rect,#mermaid-svg-I7V0XPqNaop5M0gw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I7V0XPqNaop5M0gw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I7V0XPqNaop5M0gw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I7V0XPqNaop5M0gw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Ok(v)
Err(e)
匹配
不匹配
表达式 expr
expr 的结果
解包得到 v
继续执行后续代码
错误类型匹配?
return Err(e)
调用 From::from(e)
return Err(转换后)
提前退出函数
函数正常返回
函数返回 Err

3. ? 与 Option

? 也能用在 Option 上:

rust 复制代码
fn first_char(s: &str) -> Option<char> {
    let c = s.chars().next()?;   // None 就返回 None
    Some(c)
}

规则一样:Some(v) 解包成 v,None 提前返回 None。

? 不能在 Result 和 Option 之间混用:

  • 在返回 Result 的函数里,? 只能作用于 Result
  • 在返回 Option 的函数里,? 只能作用于 Option
rust 复制代码
fn f() -> Result<i32, String> {
    let x: Option<i32> = Some(5);
    let y = x?;    // ❌ 编译错误
    Ok(y)
}

编译器会报错:

复制代码
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s,
              in a function that returns `Result`

注意错误信息里两个关键点:on Results, not Options(说明混用类型不匹配),in a function that returns Result(说明函数返回类型不对)。

混用场景需要用 ok_or 或 ok_or_else 手动转换:

rust 复制代码
fn f() -> Result<i32, String> {
    let x: Option<i32> = Some(5);
    let y = x.ok_or("没有值")?;    // ✅ Option 转 Result
    Ok(y)
}

注意 :? 只能用在返回 Result 或 Option 的函数里。在返回 () 的函数里用 ? 会编译错误。

rust 复制代码
fn main() {
    let f = File::open("a.txt")?;   // ❌ main 返回 ()
}

修复:让 main 返回 Result。

rust 复制代码
fn main() -> Result<(), std::io::Error> {
    let f = File::open("a.txt")?;   // ✅
    Ok(())
}

4. main 返回 Result

main 的返回类型可以是 Result<(), E>,其中 E: Debug:

rust 复制代码
use std::fs::File;

fn main() -> Result<(), std::io::Error> {
    let _f = File::open("hello.txt")?;
    Ok(())
}

成功时进程退出码 0。失败时 Rust 会打印错误(用 Debug),退出码非零。

为什么要求 E: Debug? 因为 Rust 需要用 {:?} 打印错误。Display 不够,因为 main 的错误信息是给开发者看的(调试用),不是给用户看的。

和 unwrap 的区别 :main 返回 Result 时,错误会被打印且退出码非零,但不会 触发栈展开。它比 unwrap 温和------没有 panic 信息,只有 Debug 输出。


五、常用辅助方法

1. unwrap 和 expect

unwrap :Ok 取值,Err 则 panic。

rust 复制代码
let f = File::open("hello.txt").unwrap();

错误时 panic 信息(Rust 1.72+):

复制代码
thread 'main' panicked at src/main.rs:2:47:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

expect :和 unwrap 一样,但能自定义 panic 信息。

rust 复制代码
let f = File::open("hello.txt").expect("无法打开文件");

错误时:

复制代码
thread 'main' panicked at src/main.rs:2:47:
无法打开文件: Os { code: 2, kind: NotFound, message: "No such file or directory" }

选择 :expect 更好,因为 panic 信息能告诉你为什么 这里不该失败。unwrap 只告诉你"失败了"。

2. unwrap_or 系列

unwrap_or:出错时返回默认值。

rust 复制代码
let n: i32 = "abc".parse().unwrap_or(0);   // 0

unwrap_or_else:出错时调用闭包生成默认值。

rust 复制代码
let n: i32 = "abc".parse().unwrap_or_else(|_| {
    eprintln!("解析失败,用默认值");
    42
});

unwrap_or_default:出错时返回该类型的默认值。

rust 复制代码
let n: i32 = "abc".parse().unwrap_or_default();     // 0
let f: f64 = "abc".parse().unwrap_or_default();     // 0.0
let b: bool = "abc".parse().unwrap_or_default();    // false

约束链 :链式调用 parse::<T>().unwrap_or_default() 要求两个 trait------T: FromStr(parse 的要求)和 T: Default(unwrap_or_default 的要求)。两者缺一不可。

单独的 unwrap_or_default 只要求 T: Default,不要求 T: FromStr。FromStr 是 parse 这一步的约束。

String 实现了 Default,但没有实现 FromStr ,所以 "abc".parse::<String>() 在 parse 这一步就编译不过------还没轮到 unwrap_or_default。这解释了为什么上面只用 i32、f64、bool 做例子。

区别 :unwrap_or 立即计算默认值(即使不需要),unwrap_or_else 惰性计算。默认值昂贵时用 unwrap_or_else。

3. 判断方法

rust 复制代码
let r: Result<i32, String> = Ok(5);

r.is_ok();      // true
r.is_err();     // false

is_ok 和 is_err 借用 &self,不消耗 r,可以连续调用。

4. 转换方法

map :转换 Ok 里的值,Err 保持不变。

rust 复制代码
let r: Result<i32, String> = Ok(5);
let doubled = r.map(|x| x * 2);   // Ok(10)

map_err :转换 Err 里的值,Ok 保持不变。

rust 复制代码
let r: Result<i32, String> = Err(String::from("bad"));
let upper = r.map_err(|e| e.to_uppercase());   // Err("BAD")

and_then :链式操作,闭包返回 Result。

rust 复制代码
let r: Result<i32, String> = Ok(5);
let result = r.and_then(|x| {
    if x > 0 { Ok(x * 2) } else { Err(String::from("must be positive")) }
});   // Ok(10)

map 和 and_then 的区别:map 把值包回 Ok,and_then 要求闭包返回 Result,避免嵌套 Ok(Ok(x))。这和第7课 Option 的 map 与 and_then 是同一个思路。

5. ok() 和 err()

ok() 和 err() 按值接收 self ,会消耗 Result。连续调用同一个 Result 会编译失败:

rust 复制代码
let r: Result<i32, String> = Ok(5);
let _ = r.ok();     // Some(5),r 被移动(let _ = 避免 #[must_use] 警告)
let _ = r.err();    // ❌ r 已被移动,编译错误

Option 和 Result 被标注了 #[must_use] ,直接丢弃返回值会产生警告。用 let _ = 明确表示"我知道这个值不重要"。上面的代码故意展示"连续调用会失败",所以第二行会报错。

想连续调用,用 .as_ref() 借用:

rust 复制代码
let r: Result<i32, String> = Ok(5);
let _ = r.as_ref().ok();     // Some(5),内部是 &i32,丢弃错误
let _ = r.as_ref().err();    // None,丢弃成功值

let e: Result<i32, String> = Err(String::from("bad"));
let _ = e.as_ref().ok();     // None
let _ = e.as_ref().err();    // Some("bad"),内部是 &String

.as_ref() 把 Result<i32, String> 转成 Result<&i32, &String>,不消耗原值。Debug 打印时会自动解引用,所以 Some(&5) 显示成 Some(5)。

ok() 把 Result 转成 Option,丢弃错误信息。适合"不关心错误,只关心有没有值"的场景。

6. ok_or 和 ok_or_else

反向转换:把 Option 转成 Result。

rust 复制代码
let opt: Option<i32> = Some(5);
let r = opt.ok_or("没有值");   // Ok(5)

let none: Option<i32> = None;
let r2 = none.ok_or("没有值"); // Err("没有值")

ok_or 立即构造错误值,ok_or_else 惰性构造。错误值昂贵时用 ok_or_else。

这对方法在 Option 和 Result 混用场景下很有用------比如要把一个 Option 通过 ? 传播到返回 Result 的函数时。


六、选择正确的错误处理方式

1. 判断标准

核心问题:这个错误,调用方能做点什么吗?

  • 能做点什么 → 返回 Result,让调用方处理
  • 做不了什么 → panic!,直接崩溃
  • 不确定 → 返回 Result,把决定权交给调用方

下面是错误处理方式选择的决策流程图:
#mermaid-svg-MksYi7vVy6dycWlQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MksYi7vVy6dycWlQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MksYi7vVy6dycWlQ .error-icon{fill:#552222;}#mermaid-svg-MksYi7vVy6dycWlQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MksYi7vVy6dycWlQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MksYi7vVy6dycWlQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MksYi7vVy6dycWlQ .marker.cross{stroke:#333333;}#mermaid-svg-MksYi7vVy6dycWlQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MksYi7vVy6dycWlQ p{margin:0;}#mermaid-svg-MksYi7vVy6dycWlQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MksYi7vVy6dycWlQ .cluster-label text{fill:#333;}#mermaid-svg-MksYi7vVy6dycWlQ .cluster-label span{color:#333;}#mermaid-svg-MksYi7vVy6dycWlQ .cluster-label span p{background-color:transparent;}#mermaid-svg-MksYi7vVy6dycWlQ .label text,#mermaid-svg-MksYi7vVy6dycWlQ span{fill:#333;color:#333;}#mermaid-svg-MksYi7vVy6dycWlQ .node rect,#mermaid-svg-MksYi7vVy6dycWlQ .node circle,#mermaid-svg-MksYi7vVy6dycWlQ .node ellipse,#mermaid-svg-MksYi7vVy6dycWlQ .node polygon,#mermaid-svg-MksYi7vVy6dycWlQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MksYi7vVy6dycWlQ .rough-node .label text,#mermaid-svg-MksYi7vVy6dycWlQ .node .label text,#mermaid-svg-MksYi7vVy6dycWlQ .image-shape .label,#mermaid-svg-MksYi7vVy6dycWlQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-MksYi7vVy6dycWlQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MksYi7vVy6dycWlQ .rough-node .label,#mermaid-svg-MksYi7vVy6dycWlQ .node .label,#mermaid-svg-MksYi7vVy6dycWlQ .image-shape .label,#mermaid-svg-MksYi7vVy6dycWlQ .icon-shape .label{text-align:center;}#mermaid-svg-MksYi7vVy6dycWlQ .node.clickable{cursor:pointer;}#mermaid-svg-MksYi7vVy6dycWlQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MksYi7vVy6dycWlQ .arrowheadPath{fill:#333333;}#mermaid-svg-MksYi7vVy6dycWlQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MksYi7vVy6dycWlQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MksYi7vVy6dycWlQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MksYi7vVy6dycWlQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MksYi7vVy6dycWlQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MksYi7vVy6dycWlQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MksYi7vVy6dycWlQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MksYi7vVy6dycWlQ .cluster text{fill:#333;}#mermaid-svg-MksYi7vVy6dycWlQ .cluster span{color:#333;}#mermaid-svg-MksYi7vVy6dycWlQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MksYi7vVy6dycWlQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MksYi7vVy6dycWlQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-MksYi7vVy6dycWlQ .icon-shape,#mermaid-svg-MksYi7vVy6dycWlQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MksYi7vVy6dycWlQ .icon-shape p,#mermaid-svg-MksYi7vVy6dycWlQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MksYi7vVy6dycWlQ .icon-shape .label rect,#mermaid-svg-MksYi7vVy6dycWlQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MksYi7vVy6dycWlQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MksYi7vVy6dycWlQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MksYi7vVy6dycWlQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 代码 bug
预期内错误
能
不能
不确定
是
否
遇到错误
错误类型
panic!
调用方能处理?
返回 Result
panic!
返回 Result
是库函数?
自定义错误枚举
Box<dyn Error> / anyhow
thiserror 简化
.context() 添加上下文
栈展开 / 终止

2. 场景对照

用 Result:

  • 文件操作(文件可能不存在)
  • 网络请求(可能超时)
  • 解析输入(可能格式错误)
  • 数据库操作(可能连接失败)
  • 库函数(不能替调用方决定)

用 panic!:

  • 索引越界(代码 bug)
  • unwrap 到 None(代码 bug)
  • 断言失败(不变量被破坏)
  • 测试代码(快速失败)
  • 原型代码(先跑通)

示例:

rust 复制代码
// 库函数:返回 Result
pub fn parse_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;
    let config = toml::from_str(&content)?;
    Ok(config)
}

// 应用程序:决定崩溃
fn main() {
    let config = parse_config("config.toml")
        .expect("配置文件格式错误,请检查 config.toml");
    // 继续使用 config
}

库函数返回 Result,把决定权交给应用程序。应用程序决定"配置错误就直接崩溃"------这是合理的,因为程序无法在无配置下运行。

3. unwrap 的使用原则

unwrap 在原型和测试里很爽,但生产代码要克制。

合理场景:

  • 测试代码
  • 已经确定不会失败(比如解析硬编码常量)
  • 不变量已由前置检查保证

不合理场景:

  • 处理用户输入
  • 文件、网络操作
  • 库函数内部
  • 任何可能失败的地方

替代方案:

rust 复制代码
// ❌ 生产代码
let config = std::fs::read_to_string("config.txt").unwrap();

// ✅ 用 expect 留下线索
let config = std::fs::read_to_string("config.txt")
    .expect("config.txt 必须存在");

// ✅ 用 ? 传播
fn load() -> Result<String, std::io::Error> {
    let config = std::fs::read_to_string("config.txt")?;
    Ok(config)
}

// ✅ 用 unwrap_or_else 给默认值
let config = std::fs::read_to_string("config.txt")
    .unwrap_or_else(|_| String::from("默认配置"));

七、自定义错误类型

1. 为什么需要

标准库的错误类型不够用。实际项目里,一个函数可能返回多种错误:

rust 复制代码
fn process(path: &str) -> Result<i32, ???> {
    let content = std::fs::read_to_string(path)?;   // io::Error
    let n = content.trim().parse::<i32>()?;          // ParseIntError
    Ok(n * 2)
}

两个 ? 返回不同错误类型,??? 填什么?

方案一 :Box<dyn Error>。

方案二:自定义错误枚举。

2. Box<dyn Error>

rust 复制代码
use std::error::Error;

fn process(path: &str) -> Result<i32, Box<dyn Error>> {
    let content = std::fs::read_to_string(path)?;
    let n = content.trim().parse::<i32>()?;
    Ok(n * 2)
}

Box<dyn Error> 是"任意错误类型"的容器。? 会把 io::Error、ParseIntError 自动转成它。

优点:简单,快速容纳多种错误。

缺点 :丢失具体错误类型,调用方无法用 match 区分错误。

适用 :脚本、原型、main 函数。

3. 自定义错误枚举

rust 复制代码
use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    NotFound(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO 错误: {}", e),
            AppError::Parse(e) => write!(f, "解析错误: {}", e),
            AppError::NotFound(s) => write!(f, "未找到: {}", s),
        }
    }
}

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            AppError::NotFound(_) => None,
        }
    }
}

// 自动转换
impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

使用:

rust 复制代码
fn process(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?;   // io::Error 自动转
    let n = content.trim().parse::<i32>()?;          // ParseIntError 自动转
    Ok(n * 2)
}

fn main() {
    match process("data.txt") {
        Ok(n) => println!("结果: {}", n),
        Err(AppError::NotFound(s)) => println!("文件不存在: {}", s),
        Err(e) => println!("其他错误: {}", e),
    }
}

关键点:

  • #[derive(Debug)] 让错误能用 {:?} 打印
  • Display 让错误能用 {} 打印给用户
  • Error trait 让错误能融入标准库生态
  • From 实现让 ? 自动转换

source 方法:返回底层错误,支持错误链。调用方可以追溯错误的根源。这是"错误链"的基础------一个高层错误可以包装底层错误,逐层追溯。

4. thiserror 和 anyhow

手写自定义错误很啰嗦。实际项目用库:

thiserror :自动生成 Display、Error、From。适合库函数。

rust 复制代码
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO 错误: {0}")]
    Io(#[from] std::io::Error),

    #[error("解析错误: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("未找到: {0}")]
    NotFound(String),
}

一个 #[derive(Error)] 加 #[from] 就搞定了上面所有手写代码。

anyhow :简化 Box<dyn Error> 的使用,适合应用程序。

rust 复制代码
use anyhow::{Context, Result};

fn process(path: &str) -> Result<i32> {
    let content = std::fs::read_to_string(path)
        .context("读取文件失败")?;
    let n = content.trim().parse::<i32>()
        .context("解析整数失败")?;
    Ok(n * 2)
}

anyhow::Result<T> 是 Result<T, anyhow::Error> 的别名。.context() 添加上下文信息。

关于 .context() 的约束 :anyhow::Context 为 Result<T, E> 实现,其中 E: Error + Send + Sync + 'static。所以 .context() 要求底层错误类型满足这三个约束。标准库的错误类型(io::Error、ParseIntError 等)都满足。但如果自定义错误类型包含 Rc、裸指针等非 Send + Sync 的字段,.context() 会编译失败。

例如:

rust 复制代码
use std::rc::Rc;

#[derive(Debug)]
struct MyError {
    data: Rc<String>,   // Rc 不是 Send + Sync
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.data)
    }
}

impl std::error::Error for MyError {}

// ❌ 编译失败:MyError 不是 Send + Sync
fn f() -> anyhow::Result<()> {
    let r: Result<(), MyError> = Err(MyError { data: Rc::new(String::new()) });
    r.context("失败")?;
    Ok(())
}

这是 anyhow 适合应用程序、不适合某些底层库场景的原因之一------它假设错误类型是线程安全的。

选择:

  • 库函数 → thiserror,具体错误类型
  • 应用程序 → anyhow,不关心具体类型

八、多元化练习题(含答案与解读)

🟢 练习 1(基础):Result 匹配

要求 :写函数 fn div(a: i32, b: i32) -> Result<i32, String>,b 为 0 时返回 Err。
参考答案

rust 复制代码
fn div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("除数不能为 0"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match div(10, 2) {
        Ok(r) => println!("结果: {}", r),
        Err(e) => println!("错误: {}", e),
    }

    match div(10, 0) {
        Ok(r) => println!("结果: {}", r),
        Err(e) => println!("错误: {}", e),
    }
}

解读 :Result<i32, String> 是简化的错误类型。实际项目会用专门的错误枚举,但用 String 入门足够。match 是处理 Result 最直接的方式。


🟢 练习 2(unwrap_or):解析默认值

要求 :把字符串 "123" 和 "abc" 解析成 i32,失败时用 0。
参考答案

rust 复制代码
fn main() {
    let a: i32 = "123".parse().unwrap_or(0);
    let b: i32 = "abc".parse().unwrap_or(0);
    println!("{} {}", a, b);   // 123 0
}

解读 :parse 返回 Result,unwrap_or(0) 在 Err 时返回 0。比 match 简洁,适合"失败就用默认值"的场景。类型标注 i32 帮助 parse 推断目标类型。


🟡 练习 3(? 运算符):改写代码

用 ? 改写:

rust 复制代码
fn parse_add(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let x = match a.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    let y = match b.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

参考答案

rust 复制代码
fn parse_add(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", parse_add("1", "2"));   // Ok(3)
    println!("{:?}", parse_add("1", "x"));   // Err(...)
}

解读 :? 让错误传播变得极简。每个 ? 相当于"成功就解包,失败就 return Err"。这行代码的可读性和直接写 x + y 几乎一样,但完整处理了错误。


🟡 练习 4(Option 与 ?):预测输出

rust 复制代码
fn get_third(v: &[i32]) -> Option<i32> {
    let x = v.get(2)?;
    Some(*x)
}

fn main() {
    println!("{:?}", get_third(&[1, 2, 3, 4]));
    println!("{:?}", get_third(&[1, 2]));
}

参考答案

复制代码
Some(3)
None

解读 :v.get(2) 返回 Option<&i32>,越界时 None。? 遇到 None 就提前返回 None。第一组有索引 2,返回 Some(3);第二组长度只有 2,索引 2 越界,返回 None。? 对 Option 和 Result 都适用。


🟡 练习 5(unwrap vs expect):选择合适方法

以下场景,你会用 unwrap、expect 还是 ??

A. 原型代码里读取一个临时文件

B. 解析硬编码的配置字符串,理论上不会失败

C. 库函数中打开用户传入的路径
参考答案

A:unwrap 就行,原型阶段快速跑通,出错崩溃也无所谓。

B:expect 更合适,因为"理论上不会失败",但万一失败了,自定义信息能帮你快速定位。比如 .expect("内置配置格式错误")。

C:必须用 ?,因为库函数不能替调用方决定"崩不崩",要把错误交回去。

解读 :unwrap 是"我确定不会错"或"原型随便",expect 是"我确定不会错,但给我留个线索",? 是"我不确定,交给上层处理"。生产代码里 ? 是主流,unwrap 越少越好。


🟡 练习 6(map_err):转换错误

要求 :把 parse::<i32>() 的 ParseIntError 转成 String。
参考答案

rust 复制代码
fn parse_int(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| format!("解析失败: {}", e))
}

fn main() {
    println!("{:?}", parse_int("42"));    // Ok(42)
    println!("{:?}", parse_int("abc"));   // Err("解析失败: invalid digit found in string")
}

解读 :map_err 转换 Err 里的值,Ok 不变。适合把底层错误转成高层错误,或统一错误类型。比 match 简洁。


🟡 练习 7(ok_or):Option 转 Result

要求 :写函数 fn get_first(v: &[i32]) -> Result<i32, String>,用 ok_or 把 Option 转成 Result。
参考答案

rust 复制代码
fn get_first(v: &[i32]) -> Result<i32, String> {
    v.first()
        .copied()
        .ok_or_else(|| String::from("列表为空"))
}

fn main() {
    println!("{:?}", get_first(&[1, 2, 3]));   // Ok(1)
    println!("{:?}", get_first(&[]));           // Err("列表为空")
}

解读 :v.first() 返回 Option<&i32>,.copied() 转成 Option<i32>,.ok_or_else(...) 转成 Result<i32, String>。ok_or_else 惰性构造错误,比 ok_or 更高效(不构造时无开销)。这个模式在需要用 ? 把 Option 传播到 Result 函数时很常见。


🔴 练习 8(综合):读文件并解析数字

要求 :写函数 fn read_number_from_file(path: &str) -> Result<i32, Box<dyn std::error::Error>>,读文件内容并解析成整数。
参考答案

rust 复制代码
use std::fs;

fn read_number_from_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;
    let n = content.trim().parse::<i32>()?;
    Ok(n)
}

fn main() {
    match read_number_from_file("num.txt") {
        Ok(n) => println!("读到: {}", n),
        Err(e) => println!("失败: {}", e),
    }
}

解读 :std::fs::read_to_string 是便利函数,内部已经处理好 File::open + Read。这里不需要 use std::io::Read,因为没用 File 上的 read_to_string 方法。Box<dyn std::error::Error> 是"任意错误类型"的容器,? 会把 io::Error、ParseIntError 自动转成它。content.trim() 去掉换行和空格。


🔴 练习 9(自定义错误):预测能否编译

rust 复制代码
use std::fmt;

#[derive(Debug)]
enum AppError {
    Empty,
    TooLong(usize),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Empty => write!(f, "不能为空"),
            AppError::TooLong(n) => write!(f, "太长: {} 字符", n),
        }
    }
}

fn check(s: &str) -> Result<(), AppError> {
    if s.is_empty() {
        return Err(AppError::Empty);
    }
    if s.len() > 10 {
        return Err(AppError::TooLong(s.len()));
    }
    Ok(())
}

fn main() {
    println!("{:?}", check(""));
    println!("{:?}", check("hello"));
    println!("{:?}", check("hello world!!!"));
}

参考答案

能编译,输出:

复制代码
Err(Empty)
Ok(())
Err(TooLong(14))

解读 :#[derive(Debug)] 让 AppError 能用 {:?} 打印。自定义 Display 让错误能用 {} 打印。check 返回 Result<(), AppError>,成功时 Ok(())(单元值)。这个模式在库设计里很常见。


🔴 练习 10(自定义错误与 From):完整实现

要求 :定义 ConfigError,包含 Io(io::Error) 和 Parse(ParseIntError) 两个变体。实现 Display、Error、From,并写一个函数同时使用两种错误。
参考答案

rust 复制代码
use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO 错误: {}", e),
            ConfigError::Parse(e) => write!(f, "解析错误: {}", e),
        }
    }
}

impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ConfigError::Io(e) => Some(e),
            ConfigError::Parse(e) => Some(e),
        }
    }
}

impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self {
        ConfigError::Io(e)
    }
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::Parse(e)
    }
}

fn load_config(path: &str) -> Result<i32, ConfigError> {
    let content = std::fs::read_to_string(path)?;   // io::Error 自动转
    let n = content.trim().parse::<i32>()?;          // ParseIntError 自动转
    Ok(n)
}

fn main() {
    match load_config("config.txt") {
        Ok(n) => println!("配置: {}", n),
        Err(e) => {
            println!("错误: {}", e);
            if let Some(source) = std::error::Error::source(&e) {
                println!("原因: {}", source);
            }
        }
    }
}

解读 :这是完整的自定义错误模式。From 实现是关键------它让 ? 能自动把 io::Error 和 ParseIntError 转成 ConfigError。source 返回底层错误,支持错误链。实际项目用 thiserror 自动生成这些代码:

rust 复制代码
#[derive(thiserror::Error, Debug)]
enum ConfigError {
    #[error("IO 错误: {0}")]
    Io(#[from] io::Error),
    #[error("解析错误: {0}")]
    Parse(#[from] ParseIntError),
}

一行顶上面几十行。


🔴 练习 11(综合):重试机制

要求 :写函数 fn retry<T, F>(times: u32, mut f: F) -> Result<T, String>,f 是可能失败的闭包,重试 times 次,全部失败返回错误。注意处理 times == 0 的边界情况。
参考答案

rust 复制代码
fn retry<T, F>(times: u32, mut f: F) -> Result<T, String>
where
    F: FnMut() -> Result<T, String>,
{
    let mut last_err: Option<String> = None;
    for i in 0..times {
        match f() {
            Ok(v) => return Ok(v),
            Err(e) => {
                eprintln!("第 {} 次失败: {}", i + 1, e);
                last_err = Some(e);
            }
        }
    }
    match last_err {
        Some(e) => Err(format!("重试 {} 次后失败: {}", times, e)),
        None => Err(String::from("没有执行任何尝试(times == 0)")),
    }
}

fn main() {
    let mut count = 0;
    let result = retry(3, || {
        count += 1;
        if count < 3 {
            Err(format!("尝试 {}", count))
        } else {
            Ok(count)
        }
    });

    println!("{:?}", result);
    // stderr 输出:
    //   第 1 次失败: 尝试 1
    //   第 2 次失败: 尝试 2
    // stdout 输出:
    //   Ok(3)

    // 边界情况:times == 0
    let result0 = retry(0, || Ok::<i32, String>(42));
    println!("{:?}", result0);   // Err("没有执行任何尝试(times == 0)")
}

解读 :用 Option<String> 而不是"魔法初始值"来记录 last_err。这样"从未执行过"和"执行过但失败"在类型上区分开,times == 0 的边界情况也能正确处理。retry 接收闭包 FnMut() -> Result<T, String>,循环调用,成功就返回,失败就记录错误继续。mut f 因为 FnMut 需要可变借用。闭包捕获 count,每次调用 +1。第 3 次成功,返回 Ok(3)。

实际场景 :网络请求重试、文件锁重试、数据库连接重试。实际项目里会用 backoff 库处理指数退避。


🔵 思考题 1:什么时候用 panic!,什么时候用 Result?

参考答案

用 panic! 的情况:

  • 代码有 bug,继续执行没意义,比如逻辑上不可能到达的分支
  • 示例、原型、测试里快速失败
  • 违反契约的情况,比如索引越界、unwrap 到 None

用 Result 的情况:

  • 错误是"预期内"的,比如文件不存在、网络超时、解析失败
  • 调用方可能有合理的处理方式,比如重试、用默认值、提示用户
  • 库函数几乎总是用 Result,把决定权交给调用方

解读 :核心判断是"这个错误,调用方能做点什么吗?"能,就 Result;不能,就 panic!。比如"配置文件格式错误"用户能改,用 Result;"内存分配失败"程序无能为力,用 panic!。

进阶思考 :panic! 是"逻辑错误"的信号,Result 是"预期错误"的信号。前者表示"代码写错了",后者表示"运行时遇到了问题"。这两类错误的处理方式本质不同。


🔵 思考题 2:? 运算符做了哪些事?

参考答案

? 做了三件事:

第一,成功解包 :Ok(v) 变成 v,继续执行。

第二,失败返回 :Err(e) 变成 return Err(e),提前退出。

第三,自动类型转换 :如果错误类型不匹配,自动调用 From::from 做类型转换。

rust 复制代码
fn f() -> Result<(), Box<dyn std::error::Error>> {
    let _n: i32 = "1".parse()?;   // ParseIntError 自动转成 Box<dyn Error>
    Ok(())
}

解读 :第三点是关键,它让 ? 能在错误类型不同的函数间传播。前提是目标错误类型实现了 From<源错误类型>。这也是为什么自定义错误常用 thiserror------它自动帮你生成 From 实现。

关于 ? 的展开 :? 大致展开成:

rust 复制代码
match expr {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

对 Option 类似,但用 None 而不是 Err。


🔵 思考题 3:unwrap 和 expect 有什么区别?生产代码能用吗?

参考答案

区别只在 panic 信息:unwrap 打印默认信息,expect 打印你提供的字符串。功能完全一样,都是"成功取值,失败崩溃"。

下面是两个独立的场景,展示 panic 信息的格式:

rust 复制代码
// 场景一:unwrap
let opt1: Option<i32> = None;
let _a = opt1.unwrap();
// thread 'main' panicked at src/main.rs:2:14:
// called `Option::unwrap()` on a `None` value

// 场景二:expect
let opt2: Option<i32> = None;
let _b = opt2.expect("配置缺失");
// thread 'main' panicked at src/main.rs:7:14:
// 配置缺失

为什么用两个变量? 因为 unwrap 和 expect 都按值接收 self(fn unwrap(self)、fn expect(self, msg)),调用后会移动 Option。用同一个变量连续调用会编译失败。

生产代码能用,但要克制。合理场景:

  • 测试代码,快速失败
  • 已经确定不会失败的情况,比如解析硬编码常量
  • 不变量已经由前置检查保证

解读 :unwrap 在原型和测试里很爽,但生产代码里每个 unwrap 都是一个潜在崩溃点。建议用 expect 至少留下线索,或者改成 ? 把错误交给上层。业界有"生产代码零 unwrap"的极端做法,实际项目里可以权衡。

更深的思考 :unwrap 的问题不是"会崩溃",而是"崩溃时没有上下文"。expect 至少告诉你"这里期望什么"。所以如果非要用,expect 优于 unwrap。


🔵 思考题 4:Box<dyn Error> 和自定义错误枚举怎么选?

参考答案

Box<dyn Error>:

  • 优点:简单,快速容纳多种错误,不用写 From 实现
  • 缺点:丢失具体错误类型,调用方无法 match 区分
  • 适用:脚本、原型、main 函数、不关心错误细节的场景

自定义错误枚举:

  • 优点:类型明确,调用方能 match 区分不同错误,能针对性处理
  • 缺点:需要写 Display、Error、From,啰嗦(可用 thiserror 简化)
  • 适用:库函数、需要调用方区分错误的场景

判断标准:

  • 调用方需要根据错误类型做不同处理 → 自定义枚举
  • 调用方只关心"成功还是失败" → Box<dyn Error>
  • 写库 → 自定义枚举(thiserror)
  • 写应用 → Box<dyn Error> 或 anyhow

解读 :这是"具体错误类型"和"抽象错误类型"的权衡。库函数暴露具体类型,让调用方有最大灵活性;应用程序不关心细节,用 Box<dyn Error> 或 anyhow 简化代码。

实际项目常见组合:

rust 复制代码
// 库:具体错误类型
#[derive(thiserror::Error, Debug)]
pub enum LibError { /* ... */ }

// 应用:用 anyhow 包装
fn main() -> anyhow::Result<()> {
    let _x = lib::do_something()?;   // LibError 自动转 anyhow::Error
    Ok(())
}

🔵 思考题 5:panic! 会释放资源吗?

参考答案

会。Rust 默认用栈展开(unwinding):panic 时逐层退出栈帧,调用每个栈帧的析构函数,释放资源。

rust 复制代码
fn main() {
    let _file = std::fs::File::open("data.txt").unwrap();
    panic!("崩溃");
    // _file 会被 drop,文件句柄被关闭
}

这是 Rust 的 RAII(Resource Acquisition Is Initialization)机制。即使 panic,栈上所有值的 Drop 都会被调用。

但有两种例外:

第一,panic = "abort" :Cargo.toml 里配置 panic = "abort" 后,panic 直接终止程序,不展开,不清理。产物更小,性能更好,适合嵌入式。

第二,std::mem::forget 或 ManuallyDrop:显式阻止析构。

解读 :栈展开是 Rust 内存安全的一部分------即使崩溃,也不会泄漏资源。对比 C 的 abort() 直接退出,什么都不清理。代价是展开有一定运行时开销,所以对性能敏感的场景可以改用 abort。

实际项目 :默认用展开(安全),嵌入式或对二进制大小敏感的场景用 abort(小、快)。


🔵 思考题 6:? 能用在 main 里吗?

参考答案

能,但 main 必须返回 Result:

rust 复制代码
use std::fs::File;

fn main() -> Result<(), std::io::Error> {
    let _f = File::open("hello.txt")?;
    Ok(())
}

如果 main 返回 ()(默认),用 ? 会编译错误:

rust 复制代码
fn main() {
    let f = File::open("a.txt")?;   // ❌ main 返回 ()
}

为什么? ? 遇到错误会 return Err(e),但 main 返回 (),类型不匹配。

main 返回 Result 的行为:

  • 成功(Ok(())):进程退出码 0
  • 失败(Err(e)):Rust 用 {:?} 打印错误,退出码非零

为什么要求 E: Debug? 因为 Rust 需要用 {:?} 打印错误。Display 不够,因为 main 的错误信息是给开发者看的(调试用),不是给用户看的。

解读 :main 返回 Result 让顶层也能用 ?,避免手写 match。适合小程序和脚本。大型应用可能在 main 里捕获错误,做更精细的处理(日志、友好提示、退出码)。


🔵 思考题 7:? 能在 Result 和 Option 之间混用吗?

参考答案

不能直接混用。

? 的行为由当前函数的返回类型决定:

  • 在返回 Result 的函数里,? 只能作用于 Result
  • 在返回 Option 的函数里,? 只能作用于 Option
rust 复制代码
fn f() -> Result<i32, String> {
    let x: Option<i32> = Some(5);
    let y = x?;    // ❌ 编译错误
    Ok(y)
}

编译器会报错:

复制代码
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s,
              in a function that returns `Result`

注意错误信息里两个关键点:on Results, not Options(说明混用类型不匹配),in a function that returns Result(说明函数返回类型不对)。

转换方法 :用 ok_or 或 ok_or_else 把 Option 转成 Result:

rust 复制代码
fn f() -> Result<i32, String> {
    let x: Option<i32> = Some(5);
    let y = x.ok_or("没有值")?;    // ✅ Option 转 Result
    Ok(y)
}

反向转换用 ok():

rust 复制代码
fn g() -> Option<i32> {
    let r: Result<i32, String> = Ok(5);
    let x = r.ok()?;    // ✅ Result 转 Option,丢弃错误
    Some(x)
}

解读 :这是 Rust 类型系统严格性的体现。? 不是"万能传播器",它必须知道当前函数返回什么,才能决定 None 还是 Err。混用场景需要显式转换,类型转换的意图清晰可见。这个设计避免了很多隐式 bug。


九、知识点总结(第8课)

两类错误 :不可恢复用 panic!,可恢复用 Result<T, E>。

panic!:打印信息、展开栈、退出。适合 bug 和不可处理的情况。默认释放资源。

Result :Ok(T) 或 Err(E),让调用方决定怎么处理。本质是枚举,用 match 处理。

? 运算符 :Ok 解包,Err 提前返回,自动 From 转换错误类型。对 Option 也可用,但不能跨类型混用。

main 返回 Result :让顶层也能用 ?,出错打印并返回非零退出码。

辅助方法:

  • 崩溃:unwrap、expect(按值消耗 self)
  • 默认值:unwrap_or、unwrap_or_else、unwrap_or_default
  • 判断:is_ok、is_err(借用 &self,不消耗值)
  • 转换:map、map_err、and_then
  • 转 Option:ok()、err()(按值消耗 self,连续调用需 .as_ref())
  • Option 转 Result:ok_or、ok_or_else

选择原则 :错误调用方能处理就用 Result,不能就用 panic!。

自定义错误 :实现 Display、Error,加 From 实现让 ? 自动转换。

Box<dyn Error> :快速容纳多种错误类型,隐含 'static 约束。适合入门、脚本、main。

thiserror :自动生成 Display、Error、From,适合库函数。

anyhow :简化 Box<dyn Error> 使用,加 .context(),适合应用程序。Context 为 Result<T, E> 实现,要求 E: Error + Send + Sync + 'static。

错误链 :Error::source() 返回底层错误,支持追溯根源。

约束链速查:

  • parse::<T>() 要求 T: FromStr
  • unwrap_or_default 要求 T: Default
  • Box<dyn Error> 隐含 'static
  • anyhow 的 .context() 要求 E: Error + Send + Sync + 'static

易错点:

  • read_to_string 需要 use std::io::Read(用 File 时)
  • String 没有实现 FromStr,不能 .parse::<String>()
  • String 实现了 Default,但不能 parse,所以不能用 unwrap_or_default
  • Box<dyn Error> 隐含 'static 约束
  • anyhow 的 .context() 要求 Send + Sync + 'static
  • ? 不能在 Result 和 Option 之间混用,需要 ok_or 或 ok()
  • ok()、err()、unwrap、expect 按值消耗 self,连续调用同一变量会编译失败,需 .as_ref() 或独立变量
  • Option 和 Result 标注了 #[must_use],丢弃返回值要用 let _ =
  • 处理边界情况(如 times == 0)时,用 Option 代替"魔法初始值"

十、下一课预告

第9课:集合类型

  • Vec<T>:动态数组
  • String 与 &str 深入
  • HashMap<K, V>:哈希表
  • 迭代器入门

📌 本课心法:

错误不是意外,是程序的一部分。Rust 用类型把错误显式化:panic! 说"我放弃",Result 说"你来决定"。? 让传播错误的代码和正常代码一样干净。写好错误处理,是从"能跑"到"可靠"的分水岭。

每个 unwrap 都是一个潜在的崩溃点,每个 ? 都是一次责任的传递。库函数返回 Result,把决定权交给调用方;应用程序用 expect 或 ?,明确表达"这里失败会怎样"。当你想用 unwrap 时,先问自己:这个错误,调用方能处理吗?能,就用 ?;不能,就用 expect 留下线索。

错误处理的最高境界不是"不崩溃",而是"崩溃时你知道为什么,不崩溃时你知道发生了什么"。而 Result、Box<dyn Error>、thiserror、anyhow 这些工具背后,都藏着 trait bound 和生命周期约束------parse 要 FromStr,unwrap_or_default 要 Default,Box<dyn Error> 要 'static,.context() 要 Send + Sync。理解这些约束链,你才能从"照着写"变成"知道为什么这么写"。

最后,边界情况往往最能体现代码质量。retry 里的 times == 0 就是例子------用 Option<String> 而不是"魔法初始值",让"从未执行"和"执行过但失败"在类型上区分开,这是 Rust 类型系统的典型用法:用类型表达状态,而不是用特殊值 。同样,unwrap / expect 按值消耗 self,连续调用同一变量会失败,用独立变量或 .as_ref() 是唯一的解法------理解每个方法的 self 形式,是写出可编译代码的前提。

相关推荐
郑同学的笔记1 小时前
【c++随笔30】C++ 注册回调函数
开发语言·c++
七夜zippoe1 小时前
启元 Q1 腿部运动学逆解:Python 算站立下蹲关节角
开发语言·python·ai·机器人·腿部运动学
熊猫钓鱼>_>1 小时前
精准碰一碰近场分享与拖拽投送实战:把文件“碰“进另一台设备【鸿蒙心迹】
开发语言·华为·harmonyos·鸿蒙·openharmony
CodeGL2 小时前
9、Python基础之IO编程
开发语言·数据库·python
曹牧2 小时前
SVN:失联项目
开发语言
传奇开心果编程3 小时前
【Rust入门练中学】第9课:集合类型
开发语言·rust·编程语言理论·集合类型·通用编程语言
龙孚信息3 小时前
跨境电商独立站的安全盲区:一次 PHP 核心漏洞的影响评估
开发语言·安全·php
牛牛牌蜡笔小新3 小时前
Java-常用类库
java·开发语言
sunsijia219833 小时前
2026年10月22日 PHP漏洞临时处理
开发语言·php