返回文章列表

文章

Rust 错误处理最佳实践

## Rust 错误处理最佳实践整合 ### 1. 区分可恢复与不可恢复错误 - **不可恢复错误(panic / expect / unwrap)** - 仅在程序进入“理论上不可能发生”的分支时使用,如数组越界、逻辑不可能为假的条件。 - `panic!`、`.expect(...)`、`.unwrap()` 会导致程序中止或在调试模式下挂起。 - 示例: ```rust let first = slice.first().expect("slice 必定非空"); ``` - **可恢复错误(Option / Result)** - 对于外部输入、I/O、网络请求等可能失败的场景,使用 `Option` 或 `Result`,并在上层处理。 --- ### 2. 优雅处理 `Option` - **避免 ****`unwrap`**** / ****`expect`**:它们在未命中时会 panic。 - **`if let`**** / ****`match`**** 分支**: ```rust if let Some(today_box) = document.select(&list_box_sel).find(|node| …) { // 找到时逻辑 } else { // 未找到时返回空或默认值 return Ok(Vec::new()); } ``` - **组合子**: ```rust let items = document .select(&sel) .find(|n| …) .map(|node| parse_items(node)) // Option> .unwrap_or_else(|| Ok(Vec::new()))?; // 统一成 Result, _> ``` --- ### 3. 利用 `Result` 与 `?` 简化错误传播 - 将 `Option` 转成 `Result`: ```rust let box_node = maybe_box .ok_or(MyError::NotFound("今天 列表 未找到"))?; ``` - 使用 `?` 自动传播错误: ```rust fn parse_items(node: &Node) -> Result, MyError> { node.select(&item_sel) .map(parse_item) // parse_item -> Result .collect() // 聚合成 Result, MyError> } ``` --- ### 4. 定义清晰的错误类型 - **小型应用**:使用 [`anyhow`](https://crates.io/crates/anyhow) - **库/大型项目**:使用 [`thiserror`](https://crates.io/crates/thiserror) 定义枚举: ```rust #[derive(thiserror::Error, Debug)] pub enum FetchError { #[error("节点未找到: {0}")] NotFound(&'static str), #[error("解析失败: {0}")] Parse(#[from] ParseError), #[error("网络错误: {0}")] Network(#[from] reqwest::Error), } ``` --- ### 5. 在应用层使用 `anyhow`(示例) ```rust // Cargo.toml // anyhow = "1.0"

use anyhow::{Context, Result, bail, ensure};

fn fetch_today_updates(doc: &Document) -> Result<Vec> { let today_box = doc .select(&list_box_sel) .find(|n| …) .with_context(|| "未找到带 ‘今天’ 的更新列表节点")?;

let items = today_box
    .select(&item_sel)
    .map(|node| {
        parse_item(node)
            .with_context(|| format!("解析项失败: {:?}", node))
    })
    .collect::<Result<_, _>>()?;

Ok(items)

}

fn parse_count(text: &str) -> Result { let count: u32 = text.trim().parse() .with_context(|| format!("解析数字失败: '{}'", text))?; ensure!(count > 0, "数量必须 > 0,实际为 {}", count); Ok(count) }

fn main() { env_logger::init(); if let Err(err) = run() { eprintln!("错误: {:#}", err); std::process::exit(1); } }

fn run() -> Result<()> { let html = std::fs::read_to_string("updates.html") .context("读取 updates.html 失败")?; let doc = scraper::Html::parse_document(&html); let updates = fetch_today_updates(&doc)?; if updates.is_empty() { println!("今日暂无更新"); } else { updates.iter().for_each(|u| println!("- [{}] {}", u.time, u.title)); } Ok(()) }

---
### 6. 边界与日志
- **库(Library)**:暴露 `Result<T, E>`,让调用者决定如何处理。
- **应用(Application)**:在 `main` 或顶层统一捕获并打印错误。
- **日志**:使用 `log` / `env_logger` 打印警告或错误信息:
	```rust
log::warn!("未找到今日列表,返回空结果");
	```
---
### 总结
1. **极少 panic**:只有真正“绝不会发生”风险处使用。
2. **Option / Result**:通过 `if let`、`match`、 `?`、 组合子清晰处理。
3. **错误类型**:应用层用 `anyhow`,库用 `thiserror`。
4. **丰富上下文**:`.context()` 提供定位信息。
5. **统一捕获**:在应用入口层处理错误,输出给用户或退出。
6. **合理日志**:在降级场景下记录,辅助调试和监控。
# 📎 参考文章
- <mention-page url="https://app.notion.com/p/2345c571bb7d8053ae7ddbf9b574a559"/> 
<empty-block/>