C++ 错误处理的痛
C++ 里错误处理方式一大堆,但没一个完美的:
// 方式1: 返回值 + 特殊值
int get_value() {
if (failed) return -1; // -1 是魔法值
}
// 方式2: 异常
try {
do_something();
} catch (const std::exception& e) {
// 异常才是正文...
}
异常的问题是:不知道会抛什么,不知道该不该 catch,析构函数里抛异常还会 std::terminate。
Rust 的错误哲学
Rust 把错误分为两类:
- 可恢复错误 →
Result<T, E> - 不可恢复错误 →
panic!
// 可恢复:用 Result
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// 不可恢复:用 panic
fn main() {
let v = vec![1, 2, 3];
v.get(10).expect("索引超出范围"); // 程序员的bug
}
实战:错误处理的几种模式
1. 基本用法
use std::fs::File;
use std::io::{self, Read};
fn read_config() -> Result<String, io::Error> {
let mut file = File::open("config.toml")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_config() {
Ok(config) => println!("配置: {}", config),
Err(e) => eprintln!("读取配置失败: {}", e),
}
}
? 操作符是灵魂——错误自动向上传播,不需要手写 match。
2. 多错误类型:用 thiserror
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("连接失败: {0}")]
ConnectionFailed(String),
#[error("查询超时")]
Timeout,
#[error("记录不存在")]
NotFound,
#[error("数据库内部错误")]
Internal(#[from] rusqlite::Error),
}
fn get_user(id: i32) -> Result<User, DatabaseError> {
let conn = Connection::open("app.db")?;
// ...
}
调用方只需要处理一种错误类型。
3. 应用程序错误:用 anyhow
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = read_config()
.context("无法读取配置文件")?;
let user = db::get_user(config.user_id)
.context("查询用户失败")?;
println!("用户: {:?}", user);
Ok(())
}
anyhow 的优势:
?可以自动收集任何实现了std::error::Error的错误- 带上下文信息,错误信息更清晰
- 适合应用程序(而非库)
4. 自定义错误转换
impl From<redis::RedisError> for AppError {
fn from(e: redis::RedisError) -> Self {
AppError::Cache(e.to_string())
}
}
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => AppError::NotFound,
_ => AppError::Database(e.to_string()),
}
}
}
错误处理 vs C++
| 场景 | C++ | Rust |
|---|---|---|
| 打开文件失败 | 返回空指针 / throw | Result<File, Error> |
| JSON 解析失败 | throw exception | Result<T, ParseError> |
| 忘记处理错误 | 编译器不报错(warning 容易被忽略) | 编译器报错! |
| 错误传播 | 手写 if/else 或 try/catch | ? 操作符 |
最佳实践
- 库用具体错误类型 — 方便调用方精确处理
- 应用用 anyhow — 快速开发,不需要定义一堆错误类型
- 慎用 panic! — 只用于真正的程序员 bug(如数组越界)
expect()用于确定不会失败的场景 — 比如unwrap()明确知道有值
代码示例:完整的服务错误处理
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("用户不存在")]
UserNotFound,
#[error("权限不足")]
PermissionDenied,
#[error("操作过于频繁")]
RateLimited,
}
pub struct UserService {
db: Database,
cache: Cache,
}
impl UserService {
pub async fn get_user(&self, id: i64) -> Result<User> {
// 先查缓存
if let Some(user) = self.cache.get(&id).await {
return Ok(user);
}
// 查数据库
let user = self.db
.find_user(id)
.await
.context("数据库查询失败")?;
match user {
Some(u) => {
self.cache.set(&id, &u).await;
Ok(u)
}
None => Err(ServiceError::UserNotFound.into()),
}
}
}
总结
Rust 的错误处理让我想起一句话:“让错误在编译期暴露,而不是在运行时.”
虽然 Result 代码量看起来多一些,但:
- 错误处理是显式的,不会漏掉
- 错误类型清晰,容易追查
?操作符让传播优雅
比 C++ 的"盲试 catch"好太多了。
下一篇文章聊聊 Rust 异步编程:Tokio 实战。