异常 vs Result
你将学到:
Result<T, E>与try/except的对比、?运算符用于简洁的错误传播、 使用thiserror的自定义错误类型、anyhow用于应用程序,以及为什么显式错误可以防止隐藏的 bug。难度: 🟡 中级
这是 Python 开发者最大的思维转变之一。Python 使用异常进行错误处理 — 错误可以从任何地方抛出,也可以在任何地方被捕获(或者干脆不处理)。
Rust 使用 Result<T, E> — 错误是必须显式处理的值。
Python 异常处理
# Python — 异常可以从任何地方抛出
import json
def load_config(path: str) -> dict:
try:
with open(path) as f:
data = json.load(f) # 可能抛出 JSONDecodeError
if "version" not in data:
raise ValueError("Missing version field")
return data
except FileNotFoundError:
print(f"Config file not found: {path}")
return {}
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
return {}
# 这个函数还能抛出什么异常?
# IOError?PermissionError?UnicodeDecodeError?
# 你无法从函数签名看出来!
Rust 基于 Result 的错误处理
#![allow(unused)]
fn main() {
// Rust — 错误是返回值,在函数签名中可见
use std::fs;
use serde_json::Value;
fn load_config(path: &str) -> Result<Value, ConfigError> {
let contents = fs::read_to_string(path) // 返回 Result
.map_err(|e| ConfigError::FileError(e.to_string()))?;
let data: Value = serde_json::from_str(&contents) // 返回 Result
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
if data.get("version").is_none() {
return Err(ConfigError::MissingField("version".to_string()));
}
Ok(data)
}
#[derive(Debug)]
enum ConfigError {
FileError(String),
ParseError(String),
MissingField(String),
}
}
关键差异
Python: Rust:
───────── ─────
- 错误是异常(抛出) - 错误是值(返回)
- 隐藏的控制流(栈展开) - 显式控制流(? 运算符)
- 无法从签名知道什么错误 - 必须在返回类型中声明错误
- 未捕获的异常在运行时崩溃 - 未处理的 Result 产生编译警告(总是处理它们)
- try/except 是可选的 - 处理 Result 是必需的
- 宽泛的 except 捕获一切 - match 臂是穷尽的
Result 的两个变体
#![allow(unused)]
fn main() {
// Result<T, E> 恰好有两个变体:
enum Result<T, E> {
Ok(T), // 成功 — 包含值(类似 Python 的返回值)
Err(E), // 失败 — 包含错误(类似 Python 的抛出异常)
}
// 使用 Result:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string()) // 类似:raise ValueError("...")
} else {
Ok(a / b) // 类似:return a / b
}
}
// 处理 Result — 类似 try/except 但显式
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {result}"),
Err(msg) => println!("Error: {msg}"),
}
}
? 运算符
? 运算符等同于让异常沿调用栈向上传播,但它是可见的和显式的。
Python — 隐式传播
# Python — 异常静默地沿调用栈向上传播
def read_username() -> str:
with open("config.txt") as f: # FileNotFoundError 传播
return f.readline().strip() # IOError 传播
def greet():
name = read_username() # 如果这里抛出,greet() 也抛出
print(f"Hello, {name}!") # 错误时跳过
# 错误传播是"不可见的" — 你必须阅读实现
# 才能知道什么异常可能逃逸。
Rust — 使用 ? 显式传播
#![allow(unused)]
fn main() {
// Rust — ? 传播错误,但在代码和签名中都可见
use std::fs;
use std::io;
fn read_username() -> Result<String, io::Error> {
let contents = fs::read_to_string("config.txt")?; // ? = Err 时传播
Ok(contents.lines().next().unwrap_or("").to_string())
}
fn greet() -> Result<(), io::Error> {
let name = read_username()?; // ? = 如果 Err,立即返回 Err
println!("Hello, {name}!"); // 只在 Ok 时执行
Ok(())
}
// ? 说:"如果是 Err,立即从这个函数返回。"
// 它像 Python 的异常传播,但:
// 1. 可见(你看到 ?)
// 2. 在返回类型中(Result<..., io::Error>)
// 3. 编译器确保你在某处处理它
}
使用 ? 链式调用
# Python — 多个可能失败的操作
def process_file(path: str) -> dict:
with open(path) as f: # 可能失败
text = f.read() # 可能失败
data = json.loads(text) # 可能失败
validate(data) # 可能失败
return transform(data) # 可能失败
# 这些都可能抛出 — 而且异常类型不同!
#![allow(unused)]
fn main() {
// Rust — 相同的链,但显式
fn process_file(path: &str) -> Result<Data, AppError> {
let text = fs::read_to_string(path)?; // ? 传播 io::Error
let data: Value = serde_json::from_str(&text)?; // ? 传播 serde 错误
let validated = validate(&data)?; // ? 传播验证错误
let result = transform(&validated)?; // ? 传播转换错误
Ok(result)
}
// 每个 ? 都是潜在的错误返回点 — 它们都可见!
}
flowchart TD
A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"]
A -->|Err| X["返回 Err(io::Error)"]
B -->|Ok| C["validate(&data)?"]
B -->|Err| Y["返回 Err(serde::Error)"]
C -->|Ok| D["transform(&validated)?"]
C -->|Err| Z["返回 Err(ValidationError)"]
D -->|Ok| E["Ok(result) ✅"]
D -->|Err| W["返回 Err(TransformError)"]
style E fill:#d4edda,stroke:#28a745
style X fill:#f8d7da,stroke:#dc3545
style Y fill:#f8d7da,stroke:#dc3545
style Z fill:#f8d7da,stroke:#dc3545
style W fill:#f8d7da,stroke:#dc3545
每个
?都是一个错误出口点 — 而 Python 的 try/except 如果不阅读代码,你无法看出哪行可能抛出异常。📌 另见:第 15 章 — 迁移模式 涵盖在真实代码库中将 Python try/except 模式转换为 Rust。
使用 thiserror 的自定义错误类型
graph TD
AE["AppError(enum)"] --> NF["NotFound<br/>{ entity, id }"]
AE --> VE["Validation<br/>{ field, message }"]
AE --> IO["Io(std::io::Error)<br/>#[from]"]
AE --> JSON["Json(serde_json::Error)<br/>#[from]"]
IO2["std::io::Error"] -->|"通过 From 自动转换"| IO
JSON2["serde_json::Error"] -->|"通过 From 自动转换"| JSON
style AE fill:#d4edda,stroke:#28a745
style NF fill:#fff3cd
style VE fill:#fff3cd
style IO fill:#fff3cd
style JSON fill:#fff3cd
style IO2 fill:#f8d7da
style JSON2 fill:#f8d7da
#[from]属性自动生成impl From<io::Error> for AppError,所以?自动将库错误转换为你的应用错误。
Python 自定义异常
# Python — 自定义异常类
class AppError(Exception):
pass
class NotFoundError(AppError):
def __init__(self, entity: str, id: int):
self.entity = entity
self.id = id
super().__init__(f"{entity} with id {id} not found")
class ValidationError(AppError):
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"Validation error on {field}: {message}")
# 用法:
def find_user(user_id: int) -> dict:
if user_id not in users:
raise NotFoundError("User", user_id)
return users[user_id]
Rust 使用 thiserror 的自定义错误
#![allow(unused)]
fn main() {
// Rust — 使用 thiserror 的错误枚举(最流行的方法)
// Cargo.toml: thiserror = "2"
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("{entity} with id {id} not found")]
NotFound { entity: String, id: i64 },
#[error("Validation error on {field}: {message}")]
Validation { field: String, message: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error), // 从 io::Error 自动转换
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error), // 从 serde 错误自动转换
}
// 用法:
fn find_user(user_id: i64) -> Result<User, AppError> {
users.get(&user_id)
.cloned()
.ok_or(AppError::NotFound {
entity: "User".to_string(),
id: user_id,
})
}
// #[from] 属性意味着 ? 自动将 io::Error 转换为 AppError::Io
fn load_users(path: &str) -> Result<Vec<User>, AppError> {
let data = fs::read_to_string(path)?; // io::Error → AppError::Io 自动
let users: Vec<User> = serde_json::from_str(&data)?; // → AppError::Json
Ok(users)
}
}
错误处理快速参考
| Python | Rust | 说明 |
|---|---|---|
raise ValueError("msg") | return Err(AppError::Validation {...}) | 显式返回错误 |
try: ... except: | match result { Ok(v) => ..., Err(e) => ... } | 穷尽处理 |
except ValueError as e: | Err(AppError::Validation { .. }) => | 模式匹配 |
raise ... from e | #[from] 属性或 .map_err() | 错误链 |
finally: | Drop trait(自动) | 确定性清理 |
with open(...): | 基于作用域的 drop(自动) | RAII 模式 |
| 异常静默传播 | ? 显式传播 | 错误类型总是在返回类型中声明 |
isinstance(e, ValueError) | matches!(e, AppError::Validation {..}) | 错误类型检查 |
练习
🏋️ 练习:解析配置值(点击展开)
挑战:编写函数 parse_port(s: &str) -> Result<u16, String>,要求:
- 拒绝空字符串,错误为
"empty input" - 解析字符串为
u16,将解析错误映射为"invalid number: {original_error}" - 拒绝小于 1024 的端口,错误为
"port {n} is privileged"
用 ""、"hello"、"80" 和 "8080" 调用它并打印结果。
🔑 解答
fn parse_port(s: &str) -> Result<u16, String> {
if s.is_empty() {
return Err("empty input".to_string());
}
let port: u16 = s.parse().map_err(|e| format!("invalid number: {e}"))?;
if port < 1024 {
return Err(format!("port {port} is privileged"));
}
Ok(port)
}
fn main() {
for input in ["", "hello", "80", "8080"] {
match parse_port(input) {
Ok(port) => println!("✅ {input} → {port}"),
Err(e) => println!("❌ {input:?} → {e}"),
}
}
}
核心要点:? 与 .map_err() 是 Rust 替代 try/except ValueError as e: raise ConfigError(...) from e 的方案。
每个错误路径在返回类型中都可见。