目标:掌握
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. 判断标准](#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:文件、网络、IOstd::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让错误能用{}打印给用户Errortrait 让错误能融入标准库生态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: FromStrunwrap_or_default要求T: DefaultBox<dyn Error>隐含'staticanyhow的.context()要求E: Error + Send + Sync + 'static
易错点:
read_to_string需要use std::io::Read(用File时)String没有实现FromStr,不能.parse::<String>()String实现了Default,但不能parse,所以不能用unwrap_or_defaultBox<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形式,是写出可编译代码的前提。