Skip to content

Rust 错误处理基础

发布于  at 10:20 AM

两类错误

在 Rust 中,我们将错误分为两大类:可恢复错误与不可恢复错误。

Rust 没有异常机制,对于可恢复的错误,提供了 Result<T,E>,对于不可恢复的错误,Rust 提供了 panic!

panic

panic 发生时,程序会默认开始执行栈展开。这意味着 Rust 会沿着调用栈的反向顺序遍历所有的调用函数,并依次清理这些函数中的数据。

如果想直接终止,可以在 Cargo.toml 中的 [profile] 中添加

[profile.release]
panic = 'abort'

失败时触发 panic

Result<T,E> 提供了一个 unwrap 的方法实现了一个 match 效果。如果返回 Ok 变体,unwrap 方法就会返回 Ok 内部的值。

返回 Err 时,则会自动调用 panic!

let mut greeting_file = File::open("hello.txt").unwrap();

exceptunwrap 提供了同样的功能,但是会传入参数字符串作为错误信息输出。unwrap 只会携带默认的信息。

在实际的生产环境中,绝大多数的 Rust 开发者 都会选择使用 expect 而不是 unwrap,因为它可以提供更多的信息来描述操作为什么应该是成功的。

可恢复错误

大部分错误其实都没有严重到需要整个程序停止运行的地步。

例如,尝试打开文件的操作会因为文件不存在而失败。在这种情形下,你也许会考虑创建该文件而不是中止进程

enum Result<T,E> {
  Ok(T),
  Err(E),
}

这里的 TE 是泛型参数。

use std::fs::File;

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

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

匹配不同的错误

如果想要匹配不同错误,可以增加内部的 match

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

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

    let greeting_file = match greeting_file_result {
        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
                );
            }
        }
    };
}

使用闭包处理错误

可以使用 unwrap_or_else 方法更加简洁地处理代码出现的 Result<T,E>

use std::fs::File;
use std::io::{ErrorKind};

fn main() {
    let _greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

传播错误

除了可以在函数内部处理错误,我们还可以将这个错误返回给调用者,让其决定应该如何做进一步处理。这个过程也被称作 传播错误(propagating error)。

手动传播错误

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

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

这个函数尝试从文件中读取用户名。如果文件不存在或无法读取,函数会将错误返回给调用者。

? 运算符简化错误传播

Rust 提供了 ? 运算符来简化错误传播的代码:

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

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

? 运算符的工作原理:

甚至可以进一步简化:

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)?;
    Ok(username)
}

或者使用更简洁的 fs::read_to_string

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

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

? 运算符的限制

? 运算符只能在返回 ResultOption 的函数中使用。在 main 函数中使用 ? 需要将返回类型改为 Result

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}

这里的 Box<dyn Error> 是一个 trait 对象,表示任何类型的错误。

自定义错误类型

在实际项目中,我们常常需要定义自己的错误类型:

use std::fmt;

#[derive(Debug)]
pub enum MyError {
    IoError(std::io::Error),
    ParseError(std::num::ParseIntError),
    CustomError(String),
}

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

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

// 实现 From trait 以支持 ? 运算符的自动转换
impl From<std::io::Error> for MyError {
    fn from(err: std::io::Error) -> MyError {
        MyError::IoError(err)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(err: std::num::ParseIntError) -> MyError {
        MyError::ParseError(err)
    }
}

使用自定义错误类型:

use std::fs;

fn process_file(path: &str) -> Result<i32, MyError> {
    let contents = fs::read_to_string(path)?;
    let number: i32 = contents.trim().parse()?;

    if number < 0 {
        return Err(MyError::CustomError("Number must be positive".to_string()));
    }

    Ok(number * 2)
}

使用第三方库简化错误处理

anyhow - 简化应用程序错误处理

对于应用程序开发,anyhow 提供了简单的错误处理方案:

use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .context("Failed to read config file")
}

thiserror - 简化库的错误定义

对于库开发,thiserror 可以大幅简化自定义错误类型的代码:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] std::io::Error),

    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },

    #[error("unknown data store error")]
    Unknown,
}

何时使用 panic! vs Result

使用 panic! 的场景:

  1. 示例代码、原型或测试 - 在快速原型开发时使用 unwrapexpect
  2. 不可能失败的情况 - 当你确定代码逻辑上不会失败时
    let home: IpAddr = "127.0.0.1".parse().unwrap();
    
  3. 违反约定的情况 - 当接收到无效参数时

使用 Result 的场景:

  1. 预期可能发生的错误 - 如文件不存在、网络连接失败
  2. 可以恢复的错误 - 调用者可能有处理或重试的策略
  3. 库代码 - 让调用者决定如何处理错误

最佳实践

  1. 在库中返回 Result,在应用中决定是否 panic

  2. 提供有意义的错误信息

    File::open("config.toml")
        .expect("Failed to open config.toml - make sure it exists in the project root")
    
  3. 使用自定义错误类型提供结构化信息

  4. 考虑错误恢复策略 - 是重试、降级服务还是返回默认值?

  5. 不要忽略错误 - 避免使用 let _ = ... 忽略 Result

总结

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

上一篇
Python PEP 8 风格备忘录
下一篇
在 Rust 使用宏与元编程