错误处理
异常 vs Result
Section titled “异常 vs Result”学习内容:
Result<T, E>vstry/except、?操作符的简洁错误传播、 使用thiserror的自定义错误类型、应用程序的anyhow,以及为什么显式错误能防止隐藏 bug。难度: 🟡 中级
这是 Python 开发者最大的思维转变之一。Python 使用异常进行错误处理——错误可以从任何地方抛出,
在任何地方捕获(或者完全不捕获)。Rust 使用 Result<T, E>——错误是值,必须显式处理。
Python 异常处理
Section titled “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 的错误处理
Section titled “Rust 基于 Result 的错误处理”// 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 变体
Section titled “两个 Result 变体”// 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}"),}? 操作符是 Rust 等价于让异常传播到调用栈上层的方式,但它是可见的、显式的。
Python——隐式传播
Section titled “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——用 ? 显式传播
Section titled “Rust——用 ? 显式传播”// Rust — ? 传播错误,但代码中可见 AND 签名中可见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. 编译器确保你在某处处理它用 ? 链式调用
Section titled “用 ? 链式调用”# Python — 多个可能失败的操作def process_file(path: str) -> dict: with open(path) as f: # 可能失败 text = f.read() # 可能失败 data = json.loads(text) # 可能失败 validate(data) # 可能失败 return transform(data) # 可能失败 # 这些都可能抛异常 — 异常类型各不相同!// 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["Return Err(io::Error)"] B -->|Ok| C["validate(&data)?"] B -->|Err| Y["Return Err(serde::Error)"] C -->|Ok| D["transform(&validated)?"] C -->|Err| Z["Return Err(ValidationError)"] D -->|Ok| E["Ok(result) ✅"] D -->|Err| W["Return 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 的自定义错误类型
Section titled “使用 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"] -->|"auto-convert via From"| IO JSON2["serde_json::Error"] -->|"auto-convert via 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 自定义异常
Section titled “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 的自定义错误
Section titled “Rust 使用 thiserror 的自定义错误”// 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::Iofn 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)}错误处理快速参考
Section titled “错误处理快速参考”| 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 的方案。
每条错误路径在返回类型中都可见。