|
| 1 | + |
| 2 | +## Rust 错误处理速查表 (Cheatsheet) |
| 3 | + |
| 4 | +### 核心理念 |
| 5 | + |
| 6 | +Rust 通过在类型系统中显式表达错误,强制开发者处理潜在的失败情况,从而构建出高可靠性的软件。核心思想是区分**可恢复的错误 (Recoverable Errors)** 和 **不可恢复的错误 (Unrecoverable Errors)**。 |
| 7 | + |
| 8 | +| 类别 | 描述 | 主要工具 | |
| 9 | +| :--- | :--- | :--- | |
| 10 | +| **可恢复错误** | 预料之中的、可以被合理处理的错误(如文件未找到、网络中断)。 | `Result<T, E>` | |
| 11 | +| **不可恢复错误** | 意料之外的、代表程序缺陷的错误(如数组越界访问)。 | `panic!` | |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +### 1. Rust 内置错误处理机制 |
| 16 | + |
| 17 | +#### `Option<T>`:处理“值可能不存在”的情况 |
| 18 | + |
| 19 | +用于表示一个值可能是 `Some(value)` 或 `None`(空)。 |
| 20 | + |
| 21 | +* **定义**:`enum Option<T> { Some(T), None }` |
| 22 | +* **常用方法**: |
| 23 | + * `unwrap()`:获取 `Some` 里的值,但如果为 `None` 则会 `panic!`。**慎用!** |
| 24 | + * `expect("message")`:与 `unwrap()` 类似,但在 `panic!` 时提供自定义消息。 |
| 25 | + * `unwrap_or(default_value)`:如果为 `None`,则返回一个默认值。 |
| 26 | + * `is_some()` / `is_none()`:检查是否包含值。 |
| 27 | + * `map(fn)` / `and_then(fn)`:对内部的值进行操作。 |
| 28 | + |
| 29 | +```rust |
| 30 | +fn find_user(id: u32) -> Option<String> { |
| 31 | + if id == 1 { Some("Alice".to_string()) } else { None } |
| 32 | +} |
| 33 | + |
| 34 | +let user = find_user(1).unwrap_or("Default User".to_string()); // "Alice" |
| 35 | +let user2 = find_user(2).unwrap_or("Default User".to_string()); // "Default User" |
| 36 | +``` |
| 37 | + |
| 38 | +#### `Result<T, E>`:处理“操作可能失败”的情况 |
| 39 | + |
| 40 | +用于表示一个操作的结果可能是 `Ok(value)`(成功)或 `Err(error)`(失败)。 |
| 41 | + |
| 42 | +* **定义**:`enum Result<T, E> { Ok(T), Err(E) }` |
| 43 | +* **常用方法**: |
| 44 | + * `unwrap()` / `expect("message")`:与 `Option` 类似,成功时返回值,失败时 `panic!`。 |
| 45 | + * `is_ok()` / `is_err()`:检查结果是成功还是失败。 |
| 46 | + * `ok()`:将 `Result<T, E>` 转换为 `Option<T>`。 |
| 47 | + * `err()`:将 `Result<T, E>` 转换为 `Option<E>`。 |
| 48 | + * `map_err(fn)`:在不改变 `Ok` 值的情况下,转换 `Err` 中的错误类型。 |
| 49 | + |
| 50 | +```rust |
| 51 | +fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> { |
| 52 | + s.parse::<i32>() |
| 53 | +} |
| 54 | + |
| 55 | +match parse_number("123") { |
| 56 | + Ok(num) => println!("Success: {}", num), // Success: 123 |
| 57 | + Err(e) => println!("Error: {}", e), |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +#### `?` 操作符:优雅地传播错误 |
| 62 | + |
| 63 | +`?` 是 Rust 错误处理的语法糖,用于简化错误传播。它只能用于返回 `Result` 或 `Option` 的函数。 |
| 64 | + |
| 65 | +* **工作原理**:如果 `Result` 是 `Ok(T)`,它会解包出 `T`;如果是 `Err(E)`,它会立即从当前函数返回 `Err(E)`。 |
| 66 | +* **前提**:当前函数的错误类型必须能通过 `From::from` trait 从 `?` 操作的错误类型转换而来。 |
| 67 | + |
| 68 | +```rust |
| 69 | +// 旧方法 (手动 match) |
| 70 | +fn read_username_from_file_old() -> Result<String, std::io::Error> { |
| 71 | + let mut f = match std::fs::File::open("username.txt") { |
| 72 | + Ok(file) => file, |
| 73 | + Err(e) => return Err(e), |
| 74 | + }; |
| 75 | + let mut s = String::new(); |
| 76 | + match f.read_to_string(&mut s) { |
| 77 | + Ok(_) => Ok(s), |
| 78 | + Err(e) => Err(e), |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// 新方法 (使用 ?) |
| 83 | +use std::io::{self, Read}; |
| 84 | +fn read_username_from_file_new() -> Result<String, io::Error> { |
| 85 | + let mut s = String::new(); |
| 86 | + // 如果 open 失败,? 会立即返回 Err |
| 87 | + std::fs::File::open("username.txt")?.read_to_string(&mut s)?; |
| 88 | + Ok(s) |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +### 2. `thiserror`:为你的库创建专业的错误类型 |
| 95 | + |
| 96 | +**目标**:为库(library/crate)定义具体的、结构化的错误类型,方便库的使用者以编程方式处理它们。 |
| 97 | + |
| 98 | +**安装**:`cargo add thiserror` |
| 99 | + |
| 100 | +**用法**: |
| 101 | + |
| 102 | +1. 创建一个 `pub enum Error`。 |
| 103 | +2. 使用 `#[derive(Error, Debug)]`。 |
| 104 | +3. 用 `#[error("...")]` 为每个变体定义错误信息。 |
| 105 | +4. 用 `#[from]` 将底层错误自动转换为你的错误类型。 |
| 106 | + |
| 107 | +**示例**: |
| 108 | + |
| 109 | +```rust |
| 110 | +// 在你的库代码中 (e.g., src/lib.rs) |
| 111 | +use thiserror::Error; |
| 112 | + |
| 113 | +// 1. 定义一个通用的 Result 别名 |
| 114 | +pub type Result<T> = std::result::Result<T, DataError>; |
| 115 | + |
| 116 | +// 2. 定义你的自定义错误枚举 |
| 117 | +#[derive(Error, Debug)] |
| 118 | +pub enum DataError { |
| 119 | + #[error("数据库连接失败")] |
| 120 | + DatabaseError(#[source] sqlx::Error), // `#[source]` 用于链接底层错误 |
| 121 | + |
| 122 | + #[error("I/O 错误: {0}")] |
| 123 | + IoError(#[from] std::io::Error), // `#[from]` 让 `?` 自动转换 io::Error |
| 124 | + |
| 125 | + #[error("找不到记录 ID: {id}")] |
| 126 | + NotFound { id: String }, |
| 127 | + |
| 128 | + #[error("输入无效: {message}")] |
| 129 | + InvalidInput { message: String }, |
| 130 | +} |
| 131 | + |
| 132 | +// 3. 在函数中使用 |
| 133 | +pub fn get_data(id: &str) -> Result<String> { |
| 134 | + if id.is_empty() { |
| 135 | + // 返回一个具体的错误变体 |
| 136 | + return Err(DataError::InvalidInput { message: "ID 不能为空".into() }); |
| 137 | + } |
| 138 | + // `?` 会自动将 std::io::Error 转换为 DataError::IoError |
| 139 | + let config = std::fs::read_to_string("config.txt")?; |
| 140 | + |
| 141 | + // ... 其他逻辑 ... |
| 142 | + Err(DataError::NotFound { id: id.to_string() }) |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +### 3. `anyhow`:简化应用程序的错误处理 |
| 149 | + |
| 150 | +**目标**:在应用程序(binary)中,当你不太关心具体的错误类型,只想轻松地传播错误并添加上下文时使用。 |
| 151 | + |
| 152 | +**安装**:`cargo add anyhow` |
| 153 | + |
| 154 | +**用法**: |
| 155 | + |
| 156 | +1. 在函数签名中使用 `anyhow::Result<T>`。 |
| 157 | +2. `?` 可以作用于任何实现了 `std::error::Error` 的错误类型。 |
| 158 | +3. 使用 `.context("...")` 或 `.with_context(|| ...)` 方法在错误传播链上添加上下文信息。 |
| 159 | + |
| 160 | +**示例**: |
| 161 | + |
| 162 | +```rust |
| 163 | +// 在你的应用程序代码中 (e.g., src/main.rs) |
| 164 | +use anyhow::{Context, Result}; |
| 165 | + |
| 166 | +// my_library 是上面用 thiserror 编写的库 |
| 167 | +use my_library::DataError; |
| 168 | + |
| 169 | +fn main() -> Result<()> { |
| 170 | + // anyhow::Result 让我们可以在 main 函数中使用 ? |
| 171 | + let data = load_user_data("user123") |
| 172 | + .with_context(|| "无法加载用户数据")?; |
| 173 | + |
| 174 | + println!("成功获取数据: {}", data); |
| 175 | + Ok(()) |
| 176 | +} |
| 177 | + |
| 178 | +fn load_user_data(id: &str) -> Result<String> { |
| 179 | + // 调用库函数,? 会自动将 DataError 包装进 anyhow::Error |
| 180 | + let data = my_library::get_data(id) |
| 181 | + .context("从 my_library 获取数据失败")?; |
| 182 | + |
| 183 | + // ... 其他逻辑 ... |
| 184 | + Ok(data) |
| 185 | +} |
| 186 | + |
| 187 | +// 当运行并出错时,anyhow 会打印出完整的错误链: |
| 188 | +// Error: 无法加载用户数据 |
| 189 | +// |
| 190 | +// Caused by: |
| 191 | +// 0: 从 my_library 获取数据失败 |
| 192 | +// 1: 找不到记录 ID: user123 |
| 193 | +``` |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +### 总结:何时使用什么? |
| 198 | + |
| 199 | +| 场景 | 推荐工具 | 为什么? | |
| 200 | +| :--- | :--- | :--- | |
| 201 | +| **编写库 (Library)** | **`thiserror`** | 创建具体的、结构化的错误类型,让调用者可以 `match` 并从容处理。 | |
| 202 | +| **编写应用程序 (Application)** | **`anyhow`** | 简单!轻松包装任何错误,添加上下文,并打印出用户友好的报告。 | |
| 203 | +| **函数可能返回空值** | **`Option<T>`** | 这是“没有值”的语义,而不是“失败”。 | |
| 204 | +| **在函数间传播错误** | **`?` 操作符** | 保持代码简洁和可读。 | |
| 205 | +| **致命的程序缺陷** | **`panic!`** | 用于指示不可恢复的状态,例如违反了代码不变量。避免在库的公共 API 中使用。 | |
0 commit comments