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 把错误分为两类:

  1. 可恢复错误Result<T, E>
  2. 不可恢复错误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
打开文件失败返回空指针 / throwResult<File, Error>
JSON 解析失败throw exceptionResult<T, ParseError>
忘记处理错误编译器不报错(warning 容易被忽略)编译器报错!
错误传播手写 if/else 或 try/catch? 操作符

最佳实践

  1. 库用具体错误类型 — 方便调用方精确处理
  2. 应用用 anyhow — 快速开发,不需要定义一堆错误类型
  3. 慎用 panic! — 只用于真正的程序员 bug(如数组越界)
  4. 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 实战。