Rust: Rust 错误处理:Result 与 ? 运算符
最后更新:2026-08-26
错误处理(Error Handling)是 Rust 最引人注目的设计之一——它不靠异常(exception)机制,而是用类型系统把"可能失败"编码进返回值类型中,编译器确保你不会忽略任何错误。
Rust 没有 try-catch,没有 throw。取而代之的是 Result<T, E> 枚举和 ? 运算符——它们让错误处理和正常业务逻辑分离开来,代码既安全又简洁。
1. 点外卖到取餐的异常处理故事
(1) 现实中的外卖流程
小明 (Xiao Ming) 今天加班,决定点一份外卖。整个流程中有多个可能出错的地方:
| 步骤 | 正常流程 | 可能出错 |
|---|---|---|
| 1. 找店铺 | 打开外卖 App,搜索店铺 | 找不到店铺 |
| 2. 加载菜单 | 浏览菜品 | 菜单载入失败(网络超时) |
| 3. 提交支付 | 付款 | 支付失败(余额不足) |
| 4. 等待制作 | 等待 30 分钟 | 店铺取消订单 |
| 5. 取餐 | 拿到外卖 | 取餐时发现订单已取消 |
每一步都可能成功(Ok)或失败(Err)。用 Rust 的思维来建模:
// ============================================
// 用 Result 模拟点外卖的每一步
// ============================================
// 定义可能的错误
#[derive(Debug)]
enum OrderError {
RestaurantNotFound,
MenuLoadFailed,
PaymentFailed(String),
OrderCancelled,
}
// 模拟找店铺
fn find_restaurant(name: &str) -> Result<String, OrderError> {
let available = vec!["PizzaHouse", "SushiBar", "NoodleShop"];
if available.contains(&name) {
Ok(format!("Found: {}", name))
} else {
Err(OrderError::RestaurantNotFound)
}
}
// 模拟加载菜单
fn load_menu(restaurant: &str) -> Result<Vec<&str>, OrderError> {
if restaurant.contains("Pizza") {
Ok(vec!["Margherita", "Pepperoni", "Hawaiian"])
} else {
Err(OrderError::MenuLoadFailed)
}
}
// 模拟支付
fn process_payment(amount: f64) -> Result<String, OrderError> {
if amount < 100.0 {
Ok(format!("Paid: ${:.2}", amount))
} else {
Err(OrderError::PaymentFailed("Insufficient balance".into()))
}
}
fn main() {
// 从头到尾跑一遍完整流程
let restaurant = find_restaurant("PizzaHouse");
match restaurant {
Ok(msg) => println!("Step 1: {}", msg),
Err(e) => println!("Step 1 failed: {:?}", e),
}
}
每一步返回
Result<T, E>——Ok(T)表示成功,Err(E)表示失败。调用者必须显式处理两种可能性,不可能"忘记处理错误"。
2. 概念图解
以下 Mermaid 流程图展示 Result<T, E> 错误处理的四种主要路径:match 精细处理、? 运算符传播、unwrap/expect 快速取值、以及 panic 崩溃:
graph TB
A["Result<T, E>"] --> B["Ok(T)<br/>成功"]
A --> C["Err(E)<br/>失败"]
B --> D["继续执行<br/>正常流程"]
C --> E["match 处理<br/>精细分支控制"]
C --> F["? 运算符<br/>向上传播错误"]
C --> G["unwrap / expect<br/>快速取值"]
E --> H["对不同错误<br/>分别处理"]
F --> I["调用者函数<br/>接收 Err"]
G --> J["panic!<br/>程序崩溃"]
H --> K["返回默认值<br/>或重试逻辑"]
I --> L["main 中 match<br/>统一处理"]
style A fill:#e1f5fe,stroke:#0288d1
style B fill:#c8e6c9,stroke:#388e3c
style C fill:#ffcdd2,stroke:#d32f2f
style J fill:#ffcdd2,stroke:#d32f2f
3. 你将学到
Result<T, E>枚举:Rust 错误处理的核心类型,Ok(T)和Err(E)两个变体unwrap/expect:快速获取值的方法及其风险?运算符:在函数间传播错误的简洁语法match处理错误:针对不同错误进行精细化处理- 自定义错误类型:实现
Display+Debug让错误信息更清晰 panic!vs 错误返回值的选择策略:什么场景该用哪种方式
4. 核心概念
graph TB
A[错误处理策略] --> B[可恢复错误<br>Recoverable]
A --> C[不可恢复错误<br>Unrecoverable]
B --> D["Result<T, E>"]
D --> E["Ok(T) 成功值"]
D --> F["Err(E) 错误值"]
F --> G["match 精细处理"]
F --> H["? 向上传播"]
F --> I["unwrap/expect 快速取值<br>(有风险)"]
C --> J["panic!"]
J --> K["程序崩溃退出"]
J --> L["适用:Bug/不可恢复状态"]
B -.-> M["自定义错误类型"]
M --> N["实现 Display + Debug"]
M --> O["From trait 转换"]
(1) 四种错误处理策略对比
| 策略 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
panic! |
不可恢复错误(如数组越界、断言失败) | 快速失败,暴露问题 | 程序直接崩溃 |
unwrap / expect |
原型开发/确定不会失败 | 代码简洁 | 出错时直接 panic,不优雅 |
match / if let |
需要对不同错误做不同处理 | 精细控制错误处理逻辑 | 代码冗长 |
? 运算符 |
函数间传播错误,上层统一处理 | 最简洁,保持主逻辑清晰 | 需要在返回 Result 的函数中使用 |
(2) Result<T, E> 方法速查
| 方法 | 签名 | 作用 | 失败时行为 |
|---|---|---|---|
unwrap() |
Result<T,E> -> T |
取出 Ok 中的值 | panic! |
expect(msg) |
Result<T,E> -> T |
取出 Ok 中的值,自定义 panic 信息 | panic!(msg) |
unwrap_or(default) |
Result<T,E> -> T |
成功返回值,失败返回默认值 | 返回默认值 |
unwrap_or_else(fn) |
Result<T,E> -> T |
成功返回值,失败执行闭包 | 执行闭包 |
is_ok() |
Result<T,E> -> bool |
判断是否成功 | — |
is_err() |
Result<T,E> -> bool |
判断是否失败 | — |
ok() |
Result<T,E> -> Option<T> |
转为 Option |
None |
err() |
Result<T,E> -> Option<E> |
转为 Option |
None |
map(fn) |
Result<T,E> -> Result<U,E> |
对成功值做转换 | 不变 |
map_err(fn) |
Result<T,E> -> Result<T,F> |
对错误值做转换 | 转换错误类型 |
and_then(fn) |
Result<T,E> -> Result<U,E> |
链式调用后续操作 | 短路 |
5. 示例
▶ 示例 1:基础 Result 与 match 处理(难度 ⭐)
// ============================================
// 用 Result 处理除法运算中的除零错误
// ============================================
fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
// 用 match 处理成功和失败两种情况
println!("=== match 处理 ===");
match safe_divide(10.0, 2.0) {
Ok(result) => println!("10 / 2 = {}", result),
Err(msg) => println!("Error: {}", msg),
}
match safe_divide(10.0, 0.0) {
Ok(result) => println!("10 / 0 = {}", result),
Err(msg) => println!("Error: {}", msg),
}
// 用 unwrap_or 提供默认值
println!("\n=== unwrap_or 默认值 ===");
let result1 = safe_divide(10.0, 2.0).unwrap_or(0.0);
let result2 = safe_divide(10.0, 0.0).unwrap_or(0.0);
println!("10 / 2 = {}", result1);
println!("10 / 0 = {} (default)", result2);
// 用 unwrap_or_else 执行闭包
println!("\n=== unwrap_or_else 闭包 ===");
let result3 = safe_divide(10.0, 2.0).unwrap_or_else(|e| {
eprintln!("Warning: {}, using default", e);
0.0
});
println!("Result: {}", result3);
// ⚠️ unwrap 会 panic(取消注释下面代码来体验)
// let crash = safe_divide(10.0, 0.0).unwrap();
// println!("Will not reach here");
}
输出:
=== match 处理 ===
10 / 2 = 5
Error: Division by zero
=== unwrap_or 默认值 ===
10 / 2 = 5
10 / 0 = 0 (default)
=== unwrap_or_else 闭包 ===
Result: 5
match处理最完整——你可以针对Ok和Err分别写处理逻辑。unwrap_or和unwrap_or_else是快捷方式:失败时提供默认值。unwrap()最危险——它假设一定成功,失败就直接崩溃。
▶ 示例 2:? 运算符——链式传播错误(难度 ⭐⭐)
// ============================================
// ? 运算符:在 Result 间传播错误
// 只有在返回 Result 的函数中才能使用 ?
// ============================================
use std::fs::File;
use std::io::{self, Read};
// 读取文件的完整内容
// ? 表示:如果 File::open 失败,立即返回 Err
// 如果 read_to_string 失败,立即返回 Err
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // 打开文件,失败就返回
let mut content = String::new();
file.read_to_string(&mut content)?; // 读取内容,失败就返回
Ok(content)
}
// 不使用 ? 的等价写法——代码量翻倍
fn read_file_without_question(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => Err(e),
}
}
// 链式调用 ?——更简洁
fn read_file_chain(path: &str) -> Result<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
fn main() {
// 分别测试读得到和读不到的文件
let files = vec!["Cargo.toml", "nonexistent.txt"];
for file in &files {
match read_file(file) {
Ok(content) => {
println!("--- {} ({} bytes) ---", file, content.len());
println!("{}", &content[..content.len().min(80)]);
}
Err(e) => {
println!("Failed to read '{}': {}", file, e);
}
}
}
}
输出(假设 Cargo.toml 存在且不为空):
--- Cargo.toml (42 bytes) ---
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
Failed to read 'nonexistent.txt': The system cannot find the file specified. (os error 2)
?运算符是 Rust 错误处理的精髓:它相当于一个"失败就提前返回"的简写。expr?等价于match expr { Ok(v) => v, Err(e) => return Err(e.into()) }。注意?会自动调用From::from做错误类型转换——这是它可以跨不同错误类型传播的关键。
▶ 示例 3:自定义错误类型——实现 Display + Debug(难度 ⭐⭐)
// ============================================
// 自定义错误类型:让你的错误信息更丰富
// 需要实现 std::fmt::Display + std::fmt::Debug
// ============================================
use std::fmt;
use std::num::ParseIntError;
// 自定义错误枚举
#[derive(Debug)]
enum AppError {
/// 输入为空
EmptyInput,
/// 解析数字失败,附带原始字符串
ParseFailed(String),
/// 数值超出允许范围
OutOfRange { value: i32, min: i32, max: i32 },
/// 除以零
DivisionByZero,
}
// 实现 Display——控制用户看到的错误信息
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::EmptyInput => {
write!(f, "Input cannot be empty")
}
AppError::ParseFailed(input) => {
write!(f, "Failed to parse '{}' as a number", input)
}
AppError::OutOfRange { value, min, max } => {
write!(f, "Value {} is out of range [{}, {}]", value, min, max)
}
AppError::DivisionByZero => {
write!(f, "Division by zero is not allowed")
}
}
}
}
// 实现 From<ParseIntError>——让 ? 运算符能自动转换
impl From<ParseIntError> for AppError {
fn from(_: ParseIntError) -> Self {
AppError::ParseFailed("unknown".into())
}
}
// 处理用户输入:解析并验证
fn process_input(input: &str, divisor: i32) -> Result<i32, AppError> {
if input.is_empty() {
return Err(AppError::EmptyInput);
}
let value: i32 = input.parse().map_err(|_| {
// 手动转换错误类型
AppError::ParseFailed(input.to_string())
})?;
if value < -100 || value > 100 {
return Err(AppError::OutOfRange {
value,
min: -100,
max: 100,
});
}
if divisor == 0 {
return Err(AppError::DivisionByZero);
}
Ok(value / divisor)
}
fn main() {
let test_cases = vec![
("42", 2, "Normal case"),
("", 1, "Empty input"),
("abc", 1, "Parse error"),
("999", 1, "Out of range"),
("50", 0, "Division by zero"),
];
for (input, divisor, description) in test_cases {
match process_input(input, divisor) {
Ok(result) => println!("[{}] OK: {}", description, result),
Err(e) => println!("[{}] Error: {}", description, e),
}
}
}
输出:
[Normal case] OK: 21
[Empty input] Error: Input cannot be empty
[Parse error] Error: Failed to parse 'abc' as a number
[Out of range] Error: Value 999 is out of range [-100, 100]
[Division by zero] Error: Division by zero is not allowed
自定义错误类型需要实现
Display(用户看到的错误信息)和Debug({:?}调试输出)。实现From<T>trait 可以让?运算符自动将某种错误类型转换为你自定义的类型——这是?能够跨类型传播的核心机制。
▶ 示例 4:panic! vs 错误返回值的选择策略(难度 ⭐⭐⭐)
// ============================================
// 演示 panic! 和错误返回值的适用场景
// panic! → 不可恢复错误(Bug/断言失败)
// Result → 可恢复错误(用户输入/IO失败)
// ============================================
// --- 适合作 panic! 的场景 ---
/// 从配置文件中读取端口号
/// 如果配置文件缺失,这属于程序 Bug,panic 是合理的
fn get_default_port() -> u16 {
// 这个值硬编码在代码中,不可能会解析失败
"8080"
.parse()
.expect("Hardcoded port number is invalid")
}
/// 只接受正整数的函数
/// 传入负数说明调用方有 Bug,panic 快速暴露问题
fn sqrt_unchecked(x: i32) -> f64 {
if x < 0 {
panic!("sqrt_unchecked called with negative value: {}", x);
}
(x as f64).sqrt()
}
// --- 适合作 Result 的场景 ---
/// 从用户输入解析数字
/// 用户输入错误的格式是正常的,应当返回 Result
fn parse_user_input(input: &str) -> Result<i32, String> {
input
.parse()
.map_err(|_| format!("'{}' is not a valid integer", input))
}
/// 计算身体质量指数(BMI)
/// 体重为 0 或负数是可能的数据错误,不是程序 Bug
fn calculate_bmi(weight_kg: f64, height_m: f64) -> Result<f64, String> {
if weight_kg <= 0.0 {
return Err("Weight must be positive".to_string());
}
if height_m <= 0.0 {
return Err("Height must be positive".to_string());
}
Ok(weight_kg / (height_m * height_m))
}
fn main() {
// 场景 1:panic 用于不可恢复错误
println!("Default port: {}", get_default_port());
// 场景 2:panic 用于断言——参数不合法
let value = 16;
println!("sqrt({}) = {}", value, sqrt_unchecked(value));
// 场景 3:Result 用于可恢复错误——用户输入
let inputs = vec!["42", "hello", "-5"];
for input in inputs {
match parse_user_input(input) {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Parse failed: {}", e),
}
}
// 场景 4:Result 用于业务逻辑验证
let bmi_cases = vec![
(70.0, 1.75),
(0.0, 1.70),
(65.0, -0.5),
];
for (weight, height) in bmi_cases {
match calculate_bmi(weight, height) {
Ok(bmi) => println!("BMI: {:.1}", bmi),
Err(e) => println!("BMI error: {}", e),
}
}
// 场景 5:expect 的 panic 信息帮助调试
let numbers = vec![10, 20, 30];
let first = numbers.first().expect("Vector should not be empty");
println!("First element: {}", first);
}
输出:
Default port: 8080
sqrt(16) = 4
Parsed: 42
Parse failed: 'hello' is not a valid integer
Parsed: -5
BMI: 22.9
BMI error: Weight must be positive
BMI error: Height must be positive
First element: 10
选择策略的核心原则:
panic!用于"程序自己的 Bug"(硬编码数据无效、传入违反约定的参数、数组越界);Result用于"外部环境或用户输入导致的异常"(IO 错误、解析失败、业务验证不通过)。通俗讲:你能修复的用 Result,你修不了的用 panic!。
6. 关于 anyhow 和 thiserror 的概念介绍
在生产级 Rust 项目中,有两个社区 crate 被广泛使用来简化错误处理:
| Crate | 主要用途 | 适用场景 | 核心特性 |
|---|---|---|---|
| anyhow | 错误传播(调用方视角) | 应用程序 main 函数、CLI 工具、脚本 | anyhow::Result<T>、.context() 给错误加上下文 |
| thiserror | 错误定义(库作者视角) | 库的公共 API 错误类型 | 用 derive 宏自动生成 Display + Error 实现 |
// anyhow 风格(概念示例,不要求运行)
// use anyhow::{Context, Result};
//
// fn read_config() -> Result<String> {
// let content = std::fs::read_to_string("config.toml")
// .context("Failed to read config file")?;
// Ok(content)
// }
// thiserror 风格(概念示例,不要求运行)
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// enum MyError {
// #[error("IO error: {0}")]
// Io(#[from] std::io::Error),
//
// #[error("Parse error: {0}")]
// Parse(#[from] std::num::ParseIntError),
// }
anyhow让你关注"怎么处理错误"而非"怎么定义错误";thiserror让你用注解代替手写Display+From实现。两者通常配合使用:库用thiserror定义细粒度错误类型,应用用anyhow统一传播。
▶ 示例 5:综合练习——多层错误传播与恢复(难度 ⭐⭐⭐)
// ============================================
// 综合示例:自定义错误 + ? 传播 + 恢复策略
// ============================================
use std::fmt;
#[derive(Debug)]
enum AppError {
ParseError(String),
ValidationError(String),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::ParseError(msg) => write!(f, "解析错误: {}", msg),
AppError::ValidationError(msg) => write!(f, "验证错误: {}", msg),
AppError::NotFound(msg) => write!(f, "未找到: {}", msg),
}
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self {
AppError::ParseError(e.to_string())
}
}
fn parse_age(input: &str) -> Result<u8, AppError> {
let age: u8 = input.parse().map_err(|_| AppError::ParseError(format!("'{}' 不是有效数字", input)))?;
if age > 150 {
return Err(AppError::ValidationError(format!("年龄 {} 不合理", age)));
}
Ok(age)
}
fn find_user(id: u32) -> Result<String, AppError> {
let users = [(1, "Alice"), (2, "Bob"), (3, "Charlie")];
users.iter()
.find(|(uid, _)| *uid == id)
.map(|(_, name)| name.to_string())
.ok_or_else(|| AppError::NotFound(format!("用户 ID={}", id)))
}
fn process_user(id_str: &str, age_str: &str) -> Result<String, AppError> {
let id: u32 = id_str.parse().map_err(|_| AppError::ParseError(format!("无效 ID: '{}'", id_str)))?;
let age = parse_age(age_str)?;
let name = find_user(id)?;
Ok(format!("用户: {}, 年龄: {}", name, age))
}
fn main() {
let test_cases = [
("1", "30"),
("2", "200"),
("5", "25"),
("abc", "30"),
("3", "abc"),
];
println!("=== 用户处理测试 ===");
for (id, age) in &test_cases {
match process_user(id, age) {
Ok(result) => println!("成功: {}", result),
Err(e) => println!("失败: {}", e),
}
}
println!("\n=== 批量处理(容错)===");
let inputs = [("1", "30"), ("5", "25"), ("2", "abc"), ("3", "20")];
let mut success_count = 0;
for (id, age) in &inputs {
match process_user(id, age) {
Ok(result) => { println!("OK: {}", result); success_count += 1; }
Err(e) => println!("跳过: {}", e),
}
}
println!("成功: {}/{}", success_count, inputs.len());
}
输出:
=== 用户处理测试 ===
成功: 用户: Alice, 年龄: 30
失败: 验证错误: 年龄 200 不合理
失败: 未找到: 用户 ID=5
失败: 解析错误: 无效 ID: 'abc'
失败: 解析错误: 'abc' 不是有效数字
=== 批量处理(容错)===
OK: 用户: Alice, 年龄: 30
跳过: 未找到: 用户 ID=5
跳过: 解析错误: 'abc' 不是有效数字
OK: 用户: Charlie, 年龄: 20
成功: 2/4
自定义
AppError枚举 +From转换 +?传播,让错误处理链清晰简洁。map_err将底层错误转为自定义类型。批量处理时用match做容错——不中断循环,记录失败并继续。
❓ 常见问题
unwrap() 是不是一种不好的实践?? 运算符和 match 处理错误,应该优先用哪个?? 传播,在需要精细处理的地方用 match。? 怎么能统一传播?From trait 自动转换。panic! 能捕获吗?Display 和 Debug?Error trait 要求两者兼备。📖 小节
Result<T, E>是 Rust 错误处理的核心——Ok(T)表示成功,Err(E)表示失败,编译器强制你处理两种情况?运算符 是错误传播的语法糖——失败时自动返回Err,成功时取出Ok中的值,还自动做错误类型转换match/unwrap_or/unwrap_or_else提供不同粒度的错误处理——从精细分支到快速兜底- 自定义错误类型 通过实现
Display + Debug让错误信息清晰可读,配合From trait实现类型转换 panic!用于不可恢复错误(程序 Bug),Result用于可恢复错误(外部环境异常)——这是 Rust 错误处理的基本选择策略anyhow和thiserror是生产级错误处理的标准工具——前者简化传播,后者简化定义
📝 作业
-
难度 ⭐:写一个函数
fn parse_age(input: &str) -> Result<u8, String>,将字符串解析为年龄(0-150)。如果解析失败或数值超出范围,返回对应的错误信息。在main中用match处理三种情况:正常年龄、非数字输入、超出范围。 -
难度 ⭐⭐:写一个嵌套调用的场景——函数 A 调用函数 B,函数 B 调用函数 C,每层都可能失败。使用
?运算符传播错误。场景:read_user_file()->parse_user_data()->validate_age()。每层返回同一自定义错误类型UserDataError(用枚举定义),包含FileNotFound、ParseFailed(String)、InvalidAge(i32)三种变体。 -
难度 ⭐⭐⭐:设计一个迷你计算器,支持
add、subtract、multiply、divide四种运算,所有运算通过?链式传播错误。要求:- 定义
CalcError枚举(DivideByZero、Overflow、InvalidOperator(String)) - 实现
Display+Debug - 用
?串联多个操作:calculate("10 + 5 * 2")这样的字符串表达式 - 提示:先按空格分割字符串,然后用
fold或循环逐个处理
- 定义