跳转至

错误处理

错误处理

📚 章节概述

Rust 没有异常,而是使用 Result<T, E>Option<T> 类型来处理可恢复错误,使用 panic! 宏处理不可恢复错误。本章将深入讲解 Rust 的错误处理机制。

🎯 学习目标

  • 理解 panic! 的使用场景
  • 掌握 Result<T, E> 类型的使用
  • 学会错误传播和转换
  • 理解 Option<T> 与错误处理的关系
  • 掌握错误处理的最佳实践

📖 不可恢复错误与 panic!

1.1 使用 panic!

panic! 宏用于处理不可恢复的错误:

Rust
fn main() {
    panic!("crash and burn");
}

1.2 panic! 的常见场景

数组越界

Rust
fn main() {
    let v = vec![1, 2, 3];
    v[99]; // panic!
}

空指针解引用

Rust
fn main() {
    let x: Option<i32> = None;
    x.unwrap(); // panic!
}

1.3 backtrace

使用 backtrace 查看错误堆栈:

Bash
$ RUST_BACKTRACE=1 cargo run

1.4 何时使用 panic!

适合使用 panic! 的情况: - 示例代码和原型 - 测试代码 - 逻辑错误(如除以零) - 安全性要求高的场景

Rust
// 测试代码
#[test]
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

// 逻辑错误
fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        panic!("Division by zero!");
    }
    a / b
}

🔍 可恢复错误与 Result

2.1 Result 类型定义

Rust
enum Result<T, E> {  // Result 表示操作成功(Ok)或失败(Err)
    Ok(T),
    Err(E),
}

2.2 使用 Result

Rust
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        }
    };
}

2.3 匹配不同的错误

Rust
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

2.4 使用 unwrap 和 expect

Rust
use std::fs::File;

fn main() {
    // unwrap: 成功时返回值,失败时 panic!
    let f = File::open("hello.txt").unwrap();  // .unwrap() 取出Ok/Some的值,否则panic

    // expect: 类似 unwrap,但可以指定错误信息
    let f = File::open("hello.txt")
        .expect("Failed to open hello.txt");
}

2.5 传播错误

使用 ? 运算符传播错误:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;  // ? 运算符:出错时自动返回Err,简化错误传播

    Ok(username)
}

等价于:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    let mut file = File::open("hello.txt")?;

    file.read_to_string(&mut username)?;
    Ok(username)
}

2.6 链式调用

Rust
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

更简洁的版本:

Rust
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    std::fs::read_to_string("hello.txt")
}

🎯 Option 与错误处理

3.1 Option 类型

Rust
enum Option<T> {
    Some(T),
    None,
}

3.2 Option 的方法

unwrap 和 expect

Rust
fn main() {
    let some_value = Some(5);
    let none_value: Option<i32> = None;

    println!("{}", some_value.unwrap()); // 5
    // println!("{}", none_value.unwrap()); // panic!

    println!("{}", none_value.unwrap_or(0)); // 0
    // println!("{}", none_value.expect("No value")); // panic! with message
}

map 和 and_then

Rust
fn main() {
    let some_value = Some(5);
    let none_value: Option<i32> = None;

    // map: 转换 Some 的值
    let doubled = some_value.map(|x| x * 2);
    println!("{:?}", doubled); // Some(10)

    // and_then: 链式调用
    let result = some_value.and_then(|x| if x > 0 { Some(x * 2) } else { None });
    println!("{:?}", result); // Some(10)
}

🔄 错误转换

4.1 自定义错误类型

Rust
use std::fmt;

#[derive(Debug)]
enum MyError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::Io(err) => write!(f, "IO error: {}", err),
            MyError::Parse(err) => write!(f, "Parse error: {}", err),
        }
    }
}

impl std::error::Error for MyError {}

4.2 使用 thiserror 简化错误处理

TOML
# Cargo.toml
[dependencies]
thiserror = "1.0"
Rust
use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

4.3 使用 anyhow 简化错误处理

TOML
# Cargo.toml
[dependencies]
anyhow = "1.0"
Rust
use anyhow::Result;

fn read_file() -> Result<String> {
    let content = std::fs::read_to_string("config.txt")?;
    Ok(content)
}

📝 错误处理最佳实践

5.1 定义清晰的错误类型

Rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("File not found: {0}")]
    FileNotFound(String),

    #[error("Invalid configuration: {0}")]
    InvalidConfig(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

5.2 提供有用的错误信息

Rust
fn read_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| ConfigError::FileNotFound(path.to_string()))?;

    Config::parse(&content)
}

5.3 使用上下文增强错误

Rust
use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .context(format!("Failed to read config file: {}", path))?;

    Config::parse(&content)
        .context("Failed to parse config")
}

📝 练习题

练习 1:Result 基础

Rust
use std::fs::File;
use std::io::Read;

// TODO: 实现一个函数,读取文件内容
// 如果文件不存在,返回 Err
// 如果读取成功,返回 Ok(String)
fn read_file_content(filename: &str) -> Result<String, std::io::Error> {
    // TODO: 实现
    todo!()
}

fn main() {
    match read_file_content("test.txt") {
        Ok(content) => println!("Content: {}", content),
        Err(e) => println!("Error: {}", e),
    }
}

练习 2:错误传播

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

// TODO: 实现一个函数,读取文件并解析为整数
// 使用 ? 运算符传播错误
fn read_and_parse(filename: &str) -> Result<i32, io::Error> {
    // TODO: 实现
    todo!()
}

fn main() {
    match read_and_parse("number.txt") {
        Ok(num) => println!("Number: {}", num),
        Err(e) => println!("Error: {}", e),
    }
}

练习 3:自定义错误

Rust
use std::fmt;

// TODO: 定义一个自定义错误类型
// 包含 IoError 和 ParseError 两种错误

// TODO: 实现 Display trait

fn main() {
    // TODO: 测试自定义错误
}

练习 4:Option 处理

Rust
// TODO: 实现一个函数,从字符串中提取数字
// 如果字符串包含数字,返回 Some(i32)
// 否则返回 None
fn extract_number(s: &str) -> Option<i32> {
    // TODO: 实现
    todo!()
}

fn main() {
    let inputs = ["abc", "123", "a1b2c3"];
    for input in &inputs {
        println!("{}: {:?}", input, extract_number(input));
    }
}

💡 最佳实践

1. 使用 Result 而不是 panic!

Rust
// 好
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

// 避免
fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        panic!("Division by zero!");
    }
    a / b
}

2. 使用 ? 传播错误

Rust
// 好
fn read_config() -> Result<Config, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("config.txt")?;
    Ok(serde_json::from_str(&content)?)
}

// 避免
fn read_config() -> Result<Config, io::Error> {
    let content = fs::read_to_string("config.txt")?;
    let config: Config = match serde_json::from_str(&content) {
        Ok(c) => c,
        Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)),
    };
    Ok(config)
}

3. 使用 thiserror 定义错误

Rust
// 好
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("Config error: {0}")]
    Config(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

// 避免
enum AppError {
    Config(String),
    Io(std::io::Error),
}

4. 使用 anyhow 简化应用代码

Rust
// 好
use anyhow::Result;

fn run() -> Result<()> {
    let config = load_config()?;
    process(&config)?;
    Ok(())
}

// 避免
fn run() -> Result<(), Box<dyn std::error::Error>> {
    let config = load_config()?;
    process(&config)?;
    Ok(())
}

⚠️ 常见错误

1. 忘记处理错误

Rust
// 错误
fn main() {
    let f = File::open("hello.txt"); // 忽略错误
}

// 正确
fn main() {
    let f = File::open("hello.txt")
        .expect("Failed to open file");
}

2. 错误类型不匹配

Rust
// 错误
fn read_file() -> Result<String, io::Error> {
    let content = fs::read_to_string("config.txt")?;
    let config: Config = serde_json::from_str(&content)?; // 编译错误
    Ok(config.to_string())
}

// 正确
fn read_file() -> Result<String, Box<dyn std::error::Error>> {
    let content = fs::read_to_string("config.txt")?;
    let config: Config = serde_json::from_str(&content)?;
    Ok(config.to_string())
}

3. 过度使用 unwrap

Rust
// 错误
fn main() {
    let content = fs::read_to_string("config.txt").unwrap();
    let config: Config = serde_json::from_str(&content).unwrap();
}

// 正确
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = fs::read_to_string("config.txt")?;
    let config: Config = serde_json::from_str(&content)?;
    Ok(())
}

📚 扩展阅读

🎯 本章小结

本章介绍了 Rust 的错误处理机制:

  • ✅ 理解 panic! 的使用场景
  • ✅ 掌握 Result<T, E> 类型的使用
  • ✅ 学会错误传播和转换
  • ✅ 理解 Option<T> 与错误处理的关系
  • ✅ 掌握错误处理的最佳实践

下一章: 我们将学习 Rust 的集合类型和迭代器,包括 Vec、HashMap、HashSet 等。