第三章:错误处理——Result、Option 与 ? 运算符
第三章:错误处理——Result、Option 与 ? 运算符
Rust 没有异常(Exception)。这不是缺陷,是设计决策。Rust 强制你显式处理每一种可能的失败,让代码更可靠、更可预测。一旦习惯,你会觉得异常机制才是反常的。
一、两种错误类型
可恢复错误(Recoverable):
→ 用 Result<T, E>
→ 文件不存在、网络超时、解析失败
→ 程序可以处理并继续运行
不可恢复错误(Unrecoverable):
→ 用 panic!
→ 访问越界、断言失败、内部逻辑错误
→ 程序应该立即终止
二、Option:可能没有值
// Option 定义(标准库):
// enum Option<T> {
// Some(T), // 有值
// None, // 没有值
// }
fn find_first_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return Some(n);
}
}
None
}
fn main() {
let numbers = vec![1, 3, 5, 6, 7];
// 方式 1:match
match find_first_even(&numbers) {
Some(n) => println!("找到偶数:{}", n),
None => println!("没有偶数"),
}
// 方式 2:if let(更简洁,只关心 Some 的情况)
if let Some(n) = find_first_even(&numbers) {
println!("偶数是:{}", n);
}
// 方式 3:unwrap_or(提供默认值)
let result = find_first_even(&numbers).unwrap_or(0);
println!("结果(默认 0):{}", result);
// 方式 4:map(变换 Some 内的值)
let doubled = find_first_even(&numbers).map(|n| n * 2);
println!("{:?}", doubled); // Some(12)
}
三、Result<T, E>:可能失败的操作
use std::fs;
use std::io;
use std::num::ParseIntError;
// 文件读取(可能失败)
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
// 字符串转数字(可能失败)
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse()
}
fn main() {
// match 处理
match read_file("config.txt") {
Ok(content) => println!("内容:{}", content),
Err(e) => println!("读取失败:{}", e),
}
// 链式处理
let result = "42"
.trim()
.parse::<i32>()
.map(|n| n * 2) // Ok 时变换值
.unwrap_or_else(|e| { // Err 时提供默认值
eprintln!("解析失败:{}", e);
0
});
println!("{}", result); // 84
}
四、? 运算符——早期返回的语法糖
use std::fs;
use std::io;
// 没有 ? 的写法(冗长)
fn read_username_from_file_verbose() -> Result<String, io::Error> {
let file_result = fs::read_to_string("username.txt");
match file_result {
Ok(s) => Ok(s),
Err(e) => Err(e),
}
}
// 使用 ? 的写法(简洁)
fn read_username_from_file() -> Result<String, io::Error> {
let s = fs::read_to_string("username.txt")?; // 失败时提前返回 Err
Ok(s)
}
// 更极致的简洁
fn read_username() -> Result<String, io::Error> {
fs::read_to_string("username.txt")
}
// ? 也适用于 Option
fn first_char(s: &str) -> Option<char> {
s.chars().next() // 直接返回 Option,不需要 ?
}
fn first_char_of_first_word(s: &str) -> Option<char> {
let word = s.split_whitespace().next()?; // None 时提前返回 None
word.chars().next()
}
五、自定义错误类型
use std::fmt;
use std::num::ParseIntError;
// 定义应用级错误
#[derive(Debug)]
enum AppError {
ParseError(ParseIntError),
NegativeNumber(i32),
TooBig(i32),
}
// 实现 Display(用于打印错误信息)
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::ParseError(e) => write!(f, "解析错误:{}", e),
AppError::NegativeNumber(n) => write!(f, "不允许负数:{}", n),
AppError::TooBig(n) => write!(f, "数字太大(最大 100):{}", n),
}
}
}
// 实现 From 让 ? 运算符自动转换错误类型
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::ParseError(e)
}
}
fn parse_positive_small(s: &str) -> Result<i32, AppError> {
let n: i32 = s.trim().parse()?; // ParseIntError 自动转换为 AppError::ParseError
if n < 0 {
return Err(AppError::NegativeNumber(n));
}
if n > 100 {
return Err(AppError::TooBig(n));
}
Ok(n)
}
fn main() {
let inputs = ["42", "-5", "200", "abc"];
for input in &inputs {
match parse_positive_small(input) {
Ok(n) => println!("{} => {}", input, n),
Err(e) => println!("{} => 错误:{}", input, e),
}
}
}
六、anyhow 与 thiserror——实战错误处理
# Cargo.toml
[dependencies]
anyhow = "1.0" # 应用代码:简化错误处理
thiserror = "1.0" # 库代码:自定义错误类型
// thiserror:库代码,提供语义清晰的错误类型
use thiserror::Error;
#[derive(Debug, Error)]
enum DatabaseError {
#[error("连接失败:{0}")]
ConnectionFailed(String),
#[error("查询失败:{query}")]
QueryFailed { query: String },
#[error("记录不存在:id={id}")]
NotFound { id: u64 },
}
// anyhow:应用代码,快速错误传播
use anyhow::{Context, Result, bail};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("无法读取配置文件:{}", path))?;
let config: Config = serde_json::from_str(&content)
.context("配置文件 JSON 格式错误")?;
if config.port == 0 {
bail!("配置文件中的端口不能为 0"); // 等价于 return Err(anyhow!("..."))
}
Ok(config)
}
// main 函数使用 anyhow::Result 可以用 ? 传播所有错误
fn main() -> Result<()> {
let config = read_config("config.json")?;
println!("端口:{}", config.port);
Ok(())
}
七、panic! 的正确使用场景
// ✓ 适合 panic! 的场景:
// 1. 程序逻辑绝对不应该到达这里
match some_option {
Some(v) => v,
None => unreachable!("已经验证过不会是 None"),
}
// 2. 原型/测试代码(临时用,生产前要改)
let v: i32 = "42".parse().unwrap(); // 临时 ok,生产要用 ?
// 3. 程序初始化(失败了就应该停止)
let config = std::env::var("DATABASE_URL")
.expect("必须设置 DATABASE_URL 环境变量"); // 配置错误,应该 panic
// ❌ 不适合 panic! 的场景:
// 用户输入、网络请求、文件操作——这些都应该用 Result
关键认知
Rust 错误处理的哲学:
Python/JS:
try {
doSomething()
} catch(e) {
// 谁知道 doSomething 可能抛出什么?
// 错误类型是隐式的,文档也不一定说清楚
}
Rust:
match doSomething() {
Ok(result) => ...,
Err(e) => ... // 错误类型在函数签名中,编译器强制处理
}
选择错误类型的规则:
- 写库(crate):用
thiserror定义具体错误类型 - 写应用(binary):用
anyhow快速传播错误 - 原型阶段:用
unwrap()/expect()先跑起来,后期再改
“Rust 强制你在写代码时思考每一个失败情况,而不是等到生产环境崩溃时才意识到。这是工程纪律,不是语言限制。”