blog/source/_posts/rust-exception.md

9.7 KiB
Raw Blame History

title tags categories keywords date
Rust中的异常处理
Rust
错误处理
错误类型
异常处理
Result
Rust
语言基础
Rust,错误,异常,诧异,Result,Ok,unwrap 2022-03-16 16:30:02

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

!!! caution "叫诧异,不叫异常" 首先需要明确一点,虽然本文中一直在说异常但是在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>来说,可以如同下例中一样来处理函数的返回值。

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>

!!! caution "" 除result.is_ok()result.is_err()以外,其他的方法都会用掉result值,所以这也是result.as_ref()result.as_mut()存在的意义。

异常冒泡

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

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

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

let stat_value = get_statistics(&student_score)?;

自定义错误类型

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

#[derive(Debug, Clone)]
pub struct ParseError {
  pub message: String,
  pub errCode: u32,
}

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

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

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

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
  }
}

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

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

错误类型的包装

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

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

#[derive(thiserror::Error, Debug)]
enum CustomError {
  ParserError(ParseError),
  #[error(transparent)]
  IOError(#[from] std::io::Error),
}

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

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

#[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>类型,所以就会产生一个十分简单的错误类型包装方案。

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

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

impl<T> From<T> for CustomError
where
  T: std::error::Error,
{
  fn from(err: T) -> Self {
    // 实际转换代码
  }
}