第三章:错误处理——Result、Option 与 ? 运算符

第三章:错误处理——Result、Option 与 ? 运算符

Rust 没有异常(Exception)。这不是缺陷,是设计决策。Rust 强制你显式处理每一种可能的失败,让代码更可靠、更可预测。一旦习惯,你会觉得异常机制才是反常的。


一、两种错误类型

可恢复错误(Recoverable):
  → 用 Result<T, E>
  → 文件不存在、网络超时、解析失败
  → 程序可以处理并继续运行

不可恢复错误(Unrecoverable):
  → 用 panic!
  → 访问越界、断言失败、内部逻辑错误
  → 程序应该立即终止

二、Option:可能没有值

// Option 定义(标准库):
// enum Option<T> {
//     Some(T),  // 有值
//     None,     // 没有值
// }

fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn main() {
    let numbers = vec![1, 3, 5, 6, 7];
    
    // 方式 1:match
    match find_first_even(&numbers) {
        Some(n) => println!("找到偶数:{}", n),
        None => println!("没有偶数"),
    }
    
    // 方式 2:if let(更简洁,只关心 Some 的情况)
    if let Some(n) = find_first_even(&numbers) {
        println!("偶数是:{}", n);
    }
    
    // 方式 3:unwrap_or(提供默认值)
    let result = find_first_even(&numbers).unwrap_or(0);
    println!("结果(默认 0):{}", result);
    
    // 方式 4:map(变换 Some 内的值)
    let doubled = find_first_even(&numbers).map(|n| n * 2);
    println!("{:?}", doubled);  // Some(12)
}

三、Result<T, E>:可能失败的操作

use std::fs;
use std::io;
use std::num::ParseIntError;

// 文件读取(可能失败)
fn read_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

// 字符串转数字(可能失败)
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.trim().parse()
}

fn main() {
    // match 处理
    match read_file("config.txt") {
        Ok(content) => println!("内容:{}", content),
        Err(e) => println!("读取失败:{}", e),
    }
    
    // 链式处理
    let result = "42"
        .trim()
        .parse::<i32>()
        .map(|n| n * 2)         // Ok 时变换值
        .unwrap_or_else(|e| {   // Err 时提供默认值
            eprintln!("解析失败:{}", e);
            0
        });
    
    println!("{}", result);  // 84
}

四、? 运算符——早期返回的语法糖

use std::fs;
use std::io;

// 没有 ? 的写法(冗长)
fn read_username_from_file_verbose() -> Result<String, io::Error> {
    let file_result = fs::read_to_string("username.txt");
    match file_result {
        Ok(s) => Ok(s),
        Err(e) => Err(e),
    }
}

// 使用 ? 的写法(简洁)
fn read_username_from_file() -> Result<String, io::Error> {
    let s = fs::read_to_string("username.txt")?;  // 失败时提前返回 Err
    Ok(s)
}

// 更极致的简洁
fn read_username() -> Result<String, io::Error> {
    fs::read_to_string("username.txt")
}

// ? 也适用于 Option
fn first_char(s: &str) -> Option<char> {
    s.chars().next()  // 直接返回 Option,不需要 ?
}

fn first_char_of_first_word(s: &str) -> Option<char> {
    let word = s.split_whitespace().next()?;  // None 时提前返回 None
    word.chars().next()
}

五、自定义错误类型

use std::fmt;
use std::num::ParseIntError;

// 定义应用级错误
#[derive(Debug)]
enum AppError {
    ParseError(ParseIntError),
    NegativeNumber(i32),
    TooBig(i32),
}

// 实现 Display(用于打印错误信息)
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::ParseError(e) => write!(f, "解析错误:{}", e),
            AppError::NegativeNumber(n) => write!(f, "不允许负数:{}", n),
            AppError::TooBig(n) => write!(f, "数字太大(最大 100):{}", n),
        }
    }
}

// 实现 From 让 ? 运算符自动转换错误类型
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::ParseError(e)
    }
}

fn parse_positive_small(s: &str) -> Result<i32, AppError> {
    let n: i32 = s.trim().parse()?;  // ParseIntError 自动转换为 AppError::ParseError
    
    if n < 0 {
        return Err(AppError::NegativeNumber(n));
    }
    if n > 100 {
        return Err(AppError::TooBig(n));
    }
    Ok(n)
}

fn main() {
    let inputs = ["42", "-5", "200", "abc"];
    
    for input in &inputs {
        match parse_positive_small(input) {
            Ok(n) => println!("{} => {}", input, n),
            Err(e) => println!("{} => 错误:{}", input, e),
        }
    }
}

六、anyhow 与 thiserror——实战错误处理

# Cargo.toml
[dependencies]
anyhow = "1.0"      # 应用代码:简化错误处理
thiserror = "1.0"   # 库代码:自定义错误类型
// thiserror:库代码,提供语义清晰的错误类型
use thiserror::Error;

#[derive(Debug, Error)]
enum DatabaseError {
    #[error("连接失败:{0}")]
    ConnectionFailed(String),
    
    #[error("查询失败:{query}")]
    QueryFailed { query: String },
    
    #[error("记录不存在:id={id}")]
    NotFound { id: u64 },
}
// anyhow:应用代码,快速错误传播
use anyhow::{Context, Result, bail};

fn read_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("无法读取配置文件:{}", path))?;
    
    let config: Config = serde_json::from_str(&content)
        .context("配置文件 JSON 格式错误")?;
    
    if config.port == 0 {
        bail!("配置文件中的端口不能为 0");  // 等价于 return Err(anyhow!("..."))
    }
    
    Ok(config)
}

// main 函数使用 anyhow::Result 可以用 ? 传播所有错误
fn main() -> Result<()> {
    let config = read_config("config.json")?;
    println!("端口:{}", config.port);
    Ok(())
}

七、panic! 的正确使用场景

// ✓ 适合 panic! 的场景:
// 1. 程序逻辑绝对不应该到达这里
match some_option {
    Some(v) => v,
    None => unreachable!("已经验证过不会是 None"),
}

// 2. 原型/测试代码(临时用,生产前要改)
let v: i32 = "42".parse().unwrap();  // 临时 ok,生产要用 ?

// 3. 程序初始化(失败了就应该停止)
let config = std::env::var("DATABASE_URL")
    .expect("必须设置 DATABASE_URL 环境变量");  // 配置错误,应该 panic

// ❌ 不适合 panic! 的场景:
// 用户输入、网络请求、文件操作——这些都应该用 Result

关键认知

Rust 错误处理的哲学

Python/JS:
  try {
    doSomething()
  } catch(e) {
    // 谁知道 doSomething 可能抛出什么?
    // 错误类型是隐式的,文档也不一定说清楚
  }

Rust:
  match doSomething() {
    Ok(result) => ...,
    Err(e) => ...  // 错误类型在函数签名中,编译器强制处理
  }

选择错误类型的规则

  • 写库(crate):用 thiserror 定义具体错误类型
  • 写应用(binary):用 anyhow 快速传播错误
  • 原型阶段:用 unwrap() / expect() 先跑起来,后期再改

“Rust 强制你在写代码时思考每一个失败情况,而不是等到生产环境崩溃时才意识到。这是工程纪律,不是语言限制。”