Rust中的异常处理

发布时间:2022-03-16 16:30
最后更新:2024-06-20 22:33
所属分类:
Rust

在其他传统语言的异常处理中,最常见的模式就是try / catch结构,C/C++如此,Java也是如此。在try / catch模式下,我们可以自由的选择想要处理的异常,甚至于可以直接忽略异常。但是在Rust中,异常却变成了一个编程人无法回避的事情。

叫诧异,不叫异常
首先需要明确一点,虽然本文中一直在说异常,但是在Rust中,对于程序运行中产生的错误并不叫异常,而是叫诧异(Panic)。虽然承认这个词翻译的十分准确,但是我习惯上还是觉得继续叫它异常吧,其实它们本质上也是十分类似的。

Rust中的异常不是程序崩溃,其行为是明确定义的,只是它表示在程序中是不应该发生的事情而已。所有的异常都是安全的,不违反任何Rust中的安全规则。不管在哪个语言中,异常都是防止错误继续扩大的武器。

Result类型

Rust中是没有异常的,函数的执行失败可以通过函数的返回值来表示。这一点与Golang中的函数返回多个值以附带表示函数的失败执行结果十分类似。但是与Golang中直接返回多个值不同,Rust 采用了一个专用的数据类型来表示函数的正常执行结果和异常执行结果,这个数据类型就是Result

一般的函数在执行结束后,要么会返回一个正常的执行结果,要么会返回一个异常,基本上不会出现既返回一个正常执行结果又返回一个异常的。所以Result类型通过携带两个泛型类型来表示函数的两种执行结果,例如Result<i64, io::Error>表示函数成功完成执行将会返回一个i64类型的值,如果出现异常将会返回一个io::Error类型的异常。

在函数中,可以通过返回Ok(value)函数执行结果来从函数中返回一个成功的结果,而通过返回Err(error_value)函数执行结果则可以从函数中返回一个错误的结果。

通过使用Result类型,Rust强制要求编程人在调用这个可能返回错误结果的函数时,必须编写错误处理逻辑,而如果没有编写错误处理逻辑,那么Rust编译将发出警告。

捕获异常

在Rust中捕获异常实际上就是对函数返回的Result类型的返回值进行处理。因为Rust没有try / catch结构,所以对于Result类型的处理是通过match表达式来完成的。

例如对于函数fn get_statistics(scores: &Vec<i32>) -> Result<i32, io::Error>来说,可以如同下例中一样来处理函数的返回值。

1
2
3
4
5
6
7
8
match get_statistics(&student_score) {
  Ok(report) => {
    println!(report);
  }
  Err(err) => {
    println!("Error occured: {}", err);
  }
}

match表达式中,Ok()Err()两个函数分别可以将Result类型返回值中携带的内容解析出来赋予它们的参数。

但是在日常的程序中,到处都使用match表达式捕获错误就有些过于啰嗦了,所以Result类型还提供了一系列的常用方法来简化对于错误的处理。这些方法中比较常用的有以下这些。

  • result.is_ok()result.is_err(),可以返回一个布尔值,分别用于判断Result类型实例中携带的是成功结果还是失败的结果。
  • result.ok(),返回一个Option<T>类型的值,如果Result类型中携带的成功值,那么会通过Some(value)返回其中的值,否则就会返回None以表示丢弃错误值。
  • result.err(),与result.ok()类似但是会返回Option<E>类型的值,并且会丢弃成功值。
  • result.unwrap_or(fallback),在Result类型实例中携带成功值的时候直接返回成功值,否则将直接返回fallback后备值。
  • result.unwrap_or_else(fallback_fn),与result.unwrap_or()类似,但是其后备值是通过闭包计算得到的,这个在使用的时候会比result.unwrap_or()更加节省内存。
  • result.unwrap(),直接返回Result类型实例中的成功值,但是如果Result类型实例中携带的是错误值,那么就会直接产生异常。
  • result.expect(message),直接返回Result类型实例中的成功值,但是如果Result类型实例中携带的是错误值,那么将会采用message作为异常信息输出到控制台。
  • reuslt.as_ref(),将类型Result<T, E>转换为Result<&T, &E>,以提供成功值和错误值的引用。
  • result.as_mut(),将类型Result<T, E>转换为Result<&mut T, &mut E>
result.is_ok()result.is_err()以外,其他的方法都会用掉result值,所以这也是result.as_ref()result.as_mut()存在的意义。

异常冒泡

在大多数情况下,在异常发生的地方捕获和处理错误是不现实的。有相当一部分的错误更适合抛给函数或者方法的调用者去处理。在这种情况下,就用到了Rust提供的沿调用栈向上传播错误的语法。

要传播错误是非常简单的,只需要在任何会产生Result类型返回值的表达式后面添加?操作符就可以了。?操作符可以根据Result类型结果中携带的值不同,决定是提取其中的成功值还是将错误值向上抛出。

例如之前定义的函数示例,如果要向上抛出错误值只需要这样做。

1
let stat_value = get_statistics(&student_score)?;

自定义错误类型

Rust这的错误实际上就是一个类型,可以通过自定义一个结构体来定义。以下示例就定义了一个十分简单的错误类型。

1
2
3
4
5
#[derive(Debug, Clone)]
pub struct ParseError {
  pub message: String,
  pub errCode: u32,
}

这个自定义的错误类型在函数中可以通过以下方式返回使用。

1
2
3
4
5
6
7
fn some_function() -> Result<i64, ParseError> {
  // ...
  return Err(ParseError {
    message: "出现意料之外的解析错误。".to_string(),
    errCode: 20
  });
}

但是仅仅这样定义异常还不能够让这个错误的行为更加接近标准库中的标准错误类型。如果想要达到接近标准错误类型的目的,还需要让其实现一些指定的特征(Trait)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
impl fmt::Display for ParseError {
  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
    write!(f, "[{}] {}", self.errCode, self.message);
  }
}

impl std::error::Error for ParseError {
  fn description(&self) -> &str {
    &self.message
  }
}

实现了这两个特征以后,自定义的新错误类型的行为就更加接近标准库的中的标准错误类型了。

对于标准库中常见的Result<T>类型,其实是Result<T, E>类型的别名。通过定义别名类型,可以省略掉那些需要反复书写的错误类型声明。这个错误类型的别名中所可以使用的错误类型可以去到类型别名定义的位置查看。

错误类型的包装

其实对于错误类型的包装,主要是使用在函数或者方法中可能能出现多种类型的错误的情况下。在函数中返回的错误类型与函数签名中定义的错误类型不一致的时候,Rust会首先尝试进行错误类型转换,但是这种转换并不能保证一定成功。如果Rust所尝试的错误类型转换没有成功,那么我们就又收获了一条类型错误。

要解决这个问题,第一个可以被想到的方法就是提供一条错误类型的转换路径。其实多种类型的错误在统一个函数中需要处理的时候,通常会将这些错误用一个枚举类型封装起来,并且实现DisplayDebugErrorFrom<T>这几个特征。这条路径手写起来比较枯燥繁琐,一般可以借助于thiserror包(用于库)或者anyhow包(用于bin程序)的辅助来提速实现。

1
2
3
4
5
6
#[derive(thiserror::Error, Debug)]
enum CustomError {
  ParserError(ParseError),
  #[error(transparent)]
  IOError(#[from] std::io::Error),
}

其中#[error()]可以用于为错误指定一条描述信息,#[from]可以为错误实现From特征。这个错误类型在函数中只需要使用?操作符抛出错误即可使用。

如果不借助thiserror包的辅助,那么所需要编写的内容大概有以下这些。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#[derive(Debug)]
pub enum CustomError {
  ParserError(ParseError),
  IOError(std::io::Error),
}

impl std::fmt::Display for CustomError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      CustomError::ParserError(parse_error) =>
        write!(f, "{}", parse_error),
      CustomError::IOError(io_error) =>
        write!(f, "{}", io_error),
    }
  }
}

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

impl From<ParseError> for CustomError {
    fn from(err: ParseError) -> Self {
        CustomError::ParserError(err)
    }
}

impl From<std::io::Error> for CustomError {
    fn from(err: std::io::Error) -> Self {
        CustomError::IOError(err)
    }
}

另一种思路是利用Rust的特性,所有的标准库错误类型都可以被转换为Box<std::error::Error>类型,所以就会产生一个十分简单的错误类型包装方案。

1
2
type CustomError = Box<std::error::Error>;
type CustomResult<T> = Result<T, CustomError>;

这样一来,程序在运行的时候,就会自动将任意错误类型转换为CustomError类型。除了可以利用?操作符进行自动类型转换以外,还可以为自定义错误类型实现From特征,来手动完成错误类型的转换。例如

1
2
3
4
5
6
7
8
impl<T> From<T> for CustomError
where
  T: std::error::Error,
{
  fn from(err: T) -> Self {
    // 实际转换代码
  }
}

使用Anyhow简化错误处理

前文提到过Anyhow这个库,这个库的主要功能是在可编译为可执行程序的应用中使用的。在使用标准库中提供的Result类型以后,就会发现在一个函数或者应用的一个功能中,经常需要抛出各种不同的错误。虽然这些错误都实现了std::error::Error特征,但是在捕获和处理它们的时候依旧是需要严格的声明它们的类型的,或者需要书写具体错误的泛型约束,所以在实际使用中就会显得有些繁琐了。

Anyhow为这种情况提供了一个更加通用和智能的Result类型,所有返回标准库中Result<T, Error>的位置,都可以使用Result<T, anyhow::Error>或者anyhow::Result<T>来代替,而且Anyhow还可以在Result类型中记录上下文信息。

以下是一个使用Anyhow库的示例,从其中可以看出,使用Anyhow来简化标准库中的错误处理并不复杂。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 如果需要使用向Result注入上下文功能,就必须引入Context,
// 因为Context修改了Result的实现,如果不显式引入,就不能够被使用。
use anyhow::{Context, Result};

fn load_configuration(path: &Path) -> Result<Configuration> {
  // .with_context()可以将一个闭包返回的字符串作为上下文内容附加进Result。
  // .context()可以直接附加一个字符串到Result中。
  let content = std::fs::read_to_string(path).with_context(|| format!("Unabled to read file {}", path))?;
  let config: Configuration = serde_json::from_str(&content)?;
  Ok(config)
}

除了Result类型和Context特征以外,Anyhow还提供了几个比较有用的宏。

  • anyhow!可以用来利用字符串直接产生一个anyhow::Error
  • bail!相当于return Err(anyhow!($args...));,可以用来快速返回一个错误。
  • ensure!相当与if !expr { return Err(anyhow!($args...)); },也就是当指定条件不满足的时候快速返回一个错误。
在使用bail!ensure!两个宏的时候,需要确保函数返回的值类型为Result<T, anyhow::Error>或者anyhow::Result<T>类型。

索引标签
Rust
错误处理
错误类型
异常处理
Result