🦀 第08课:错误处理(Result/Option)

Rust没有异常机制,而是通过Result<T, E>Option<T>在类型系统中表达可能失败的操作。这让错误处理变得显式、安全且可预测。本课将全面掌握Rust的错误处理哲学。

核心特性 第8/25课

学习目标:深入Result和Option、掌握?运算符、学会自定义错误类型、理解panic vs Result的选择

📊 Rust错误处理哲学

可恢复错误Result<T, E>:文件不存在、网络超时、解析失败……调用者应该决定如何处理

不可恢复错误panic!:数组越界、逻辑不可能的情况……程序应该立即停止

可能缺失的值Option<T>:查找失败、可选配置……这不是"错误",只是"没有值"

🔴 panic! —— 不可恢复错误

fn main() {
    // panic会立即终止当前线程
    // panic!("出错了!");  // 💥 程序崩溃
    
    // 常见的自动panic场景
    // let v = vec![1, 2, 3];
    // v[99];  // ❌ 数组越界 → panic
    
    // Option的unwrap在None时panic
    // let x: Option = None;
    // x.unwrap();  // ❌ panic: called `Option::unwrap()` on a `None` value
    
    // 安全替代
    let x: Option = None;
    println!("默认值: {}", x.unwrap_or(0));
    
    // panic会展开调用栈(默认),可以设置直接终止
    // Cargo.toml: [profile.release] panic = "abort"
}
默认值: 0

✅ 验证通过

🟢 Result<T, E> —— 可恢复错误

enum Result<T, E> {
    Ok(T),    // 成功,包含值T
    Err(E),   // 失败,包含错误E
}
use std::fs;
use std::io;
use std::num::ParseIntError;

fn main() {
    // 打开文件返回Result
    let result = fs::read_to_string("nonexistent.txt");
    match result {
        Ok(content) => println!("内容: {}", content),
        Err(e) => println!("读取失败: {}", e),  // 实际会走这里
    }
    
    // 更简洁的处理方式
    // unwrap —— Ok返回值,Err则panic
    // let content = fs::read_to_string("nonexistent.txt").unwrap();  // ❌ panic!
    
    // expect —— 同unwrap但可自定义消息
    // let content = fs::read_to_string("nonexistent.txt").expect("必须存在的文件");  // ❌ panic!
    
    // unwrap_or_default —— 错误时返回默认值
    let content = fs::read_to_string("nonexistent.txt").unwrap_or_default();
    println!("默认内容: '{}'", content);  // 空字符串
    
    // unwrap_or —— 错误时返回指定值
    let content = fs::read_to_string("nonexistent.txt")
        .unwrap_or(String::from("(文件不存在)"));
    println!("替代内容: {}", content);
}
读取失败: No such file or directory (os error 2) 默认内容: '' 替代内容: (文件不存在)

✅ 验证通过

❓ 问号运算符(?)

?是Rust错误处理的语法糖,让错误传播变得简洁:

use std::fs::File;
use std::io::{self, Read};

// 不用?的写法(啰嗦)
fn read_username_from_file_verbose() -> Result<String, io::Error> {
    let file = match File::open("username.txt") {
        Ok(f) => f,
        Err(e) => return Err(e),  // 手动传播错误
    };
    let mut username = String::new();
    match file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),  // 手动传播错误
    }
}

// 用?的写法(简洁)
fn read_username_from_file() -> Result<String, io::Error> {
    let mut file = File::open("username.txt")?;  // Err自动返回
    let mut username = String::new();
    file.read_to_string(&mut username)?;  // Err自动返回
    Ok(username)
}

// 更简洁的链式调用
fn read_username_chain() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("username.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

// ?也可以用在Option上
fn last_char_of_first_line(text: &str) -> Option<char> {
    let line = text.lines().next()?;  // None自动返回
    line.chars().last()               // None自动返回
}

fn main() {
    // ?在main中的使用(main返回Result)
    let result = read_username_from_file();
    match result {
        Ok(name) => println!("用户名: {}", name),
        Err(e) => println!("读取失败: {}", e),
    }
    
    // Option上的?
    let text = "Hello\nWorld";
    println!("第一行最后一个字符: {:?}", last_char_of_first_line(text));
    println!("空字符串: {:?}", last_char_of_first_line(""));
}
读取失败: No such file or directory (os error 2) 第一行最后一个字符: Some('o') 空字符串: None

✅ 验证通过

💡 ?运算符规则:
- 在Result上下文中:Err(e)会自动转换为返回类型的Err
- 在Option上下文中:None会自动返回None
- 函数的返回类型必须兼容(Result不能?到Option中,反之亦然)

🎨 自定义错误类型

use std::fmt;
use std::error::Error;
use std::num::ParseIntError;

// 自定义错误类型
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(ParseIntError),
    NotFound(String),
    InvalidInput { field: String, reason: String },
}

// 实现Display
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO错误: {}", e),
            AppError::Parse(e) => write!(f, "解析错误: {}", e),
            AppError::NotFound(name) => write!(f, "未找到: {}", name),
            AppError::InvalidInput { field, reason } => {
                write!(f, "无效输入 '{}': {}", field, reason)
            }
        }
    }
}

// 实现Error特征
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

// 从其他错误类型转换
impl From for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// 使用自定义错误
fn parse_config(value: &str) -> Result<i32, AppError> {
    if value.is_empty() {
        return Err(AppError::InvalidInput {
            field: "value".to_string(),
            reason: "不能为空".to_string(),
        });
    }
    let num: i32 = value.parse()?;  // ParseIntError自动转为AppError
    Ok(num)
}

fn main() {
    // 正常解析
    match parse_config("42") {
        Ok(n) => println!("配置值: {}", n),
        Err(e) => println!("错误: {}", e),
    }
    
    // 解析失败
    match parse_config("abc") {
        Ok(n) => println!("配置值: {}", n),
        Err(e) => println!("错误: {}", e),
    }
    
    // 空输入
    match parse_config("") {
        Ok(n) => println!("配置值: {}", n),
        Err(e) => println!("错误: {}", e),
    }
    
    // 遍历错误链
    if let Err(e) = parse_config("xyz") {
        println!("\n错误详情:");
        println!("  描述: {}", e);
        if let Some(source) = e.source() {
            println!("  原因: {}", source);
        }
    }
}
配置值: 42 错误: 解析错误: invalid digit found in string 错误: 无效输入 'value': 不能为空 错误详情: 描述: 解析错误: invalid digit found in string 原因: invalid digit found in string

✅ 验证通过

🛠️ thiserror和anyhow

社区最常用的两个错误处理库,让错误处理更简洁:

// thiserror —— 用于库代码,自动实现Error特征
// Cargo.toml: thiserror = "2"
//
// #[derive(thiserror::Error, Debug)]
// enum AppError {
//     #[error("IO错误: {0}")]
//     Io(#[from] std::io::Error),
//     #[error("未找到: {0}")]
//     NotFound(String),
// }

// anyhow —— 用于应用代码,统一错误类型
// Cargo.toml: anyhow = "1"
//
// use anyhow::{Context, Result};
// fn read_config() -> Result {
//     let content = fs::read_to_string("config.toml")
//         .context("无法读取配置文件")?;
//     Ok(parse(content))
// }

fn main() {
    println!("thiserror: 用于库,提供类型化错误");
    println!("anyhow: 用于应用,简化错误处理");
    println!("选择原则: 库→thiserror, 应用→anyhow");
}

🏗️ 综合实战:简易配置解析器

use std::collections::HashMap;
use std::fmt;

#[derive(Debug)]
enum ConfigError {
    MissingKey(String),
    InvalidType { key: String, expected: String, got: String },
    ParseError { key: String, value: String, reason: String },
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::MissingKey(k) => write!(f, "缺少必需配置项: {}", k),
            ConfigError::InvalidType { key, expected, got } => {
                write!(f, "配置项'{}'类型错误: 期望{},实际{}", key, expected, got)
            }
            ConfigError::ParseError { key, value, reason } => {
                write!(f, "配置项'{}'值'{}'解析失败: {}", key, value, reason)
            }
        }
    }
}

struct Config {
    values: HashMap<String, String>,
}

impl Config {
    fn new() -> Self {
        Config { values: HashMap::new() }
    }
    
    fn from_str(s: &str) -> Result<Self, ConfigError> {
        let mut config = Config::new();
        for line in s.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            if let Some((key, value)) = line.split_once('=') {
                config.values.insert(key.trim().to_string(), value.trim().to_string());
            }
        }
        Ok(config)
    }
    
    fn get(&self, key: &str) -> Result<&str, ConfigError> {
        self.values.get(key)
            .map(|s| s.as_str())
            .ok_or_else(|| ConfigError::MissingKey(key.to_string()))
    }
    
    fn get_int(&self, key: &str) -> Result<i32, ConfigError> {
        let value = self.get(key)?;
        value.parse().map_err(|_| ConfigError::ParseError {
            key: key.to_string(),
            value: value.to_string(),
            reason: "不是有效的整数".to_string(),
        })
    }
    
    fn get_bool(&self, key: &str) -> Result<bool, ConfigError> {
        let value = self.get(key)?;
        match value {
            "true" | "yes" | "1" => Ok(true),
            "false" | "no" | "0" => Ok(false),
            _ => Err(ConfigError::InvalidType {
                key: key.to_string(),
                expected: "布尔值".to_string(),
                got: value.to_string(),
            }),
        }
    }
    
    fn get_float(&self, key: &str) -> Result<f64, ConfigError> {
        let value = self.get(key)?;
        value.parse().map_err(|_| ConfigError::ParseError {
            key: key.to_string(),
            value: value.to_string(),
            reason: "不是有效的浮点数".to_string(),
        })
    }
}

fn main() {
    let config_text = r#"
# 服务器配置
host = 127.0.0.1
port = 8080
debug = true
timeout = 30.5
"#;
    
    let config = Config::from_str(config_text).expect("配置解析失败");
    
    // 读取各种类型
    println!("host: {}", config.get("host").unwrap_or("localhost"));
    println!("port: {}", config.get_int("port").unwrap_or(3000));
    println!("debug: {}", config.get_bool("debug").unwrap_or(false));
    println!("timeout: {:.1}s", config.get_float("timeout").unwrap_or(10.0));
    
    // 错误情况
    println!("\n--- 错误处理演示 ---");
    match config.get("missing_key") {
        Ok(v) => println!("值: {}", v),
        Err(e) => println!("❌ {}", e),
    }
    
    match config.get_int("host") {
        Ok(v) => println!("端口号: {}", v),
        Err(e) => println!("❌ {}", e),
    }
    
    match config.get_bool("port") {
        Ok(v) => println!("布尔: {}", v),
        Err(e) => println!("❌ {}", e),
    }
}
host: 127.0.0.1 port: 8080 debug: true timeout: 30.5s --- 错误处理演示 --- ❌ 缺少必需配置项: missing_key ❌ 配置项'host'值'127.0.0.1'解析失败: 不是有效的整数 ❌ 配置项'port'类型错误: 期望布尔值,实际8080

✅ 验证通过

📝 练习

练习1:安全除法

编写safe_divide(a: f64, b: f64) -> Result<f64, String>,处理除零和溢出。

练习2:用户输入解析

编写函数解析用户输入的年龄(字符串→u8),处理各种错误情况(非数字、超出范围等)。

练习3:多步骤操作

模拟:读取文件→解析JSON→验证数据。用?运算符链式处理,自定义错误类型支持From转换。

🏆 本课成就

🔒 下一课解锁:泛型 —— 编写通用的、可复用的代码