blog/source/_posts/rust-exception.md

195 lines
9.7 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Rust中的异常处理
tags:
- Rust
- 错误处理
- 错误类型
- 异常处理
- Result
categories:
- - Rust
- 语言基础
keywords: 'Rust,错误,异常,诧异,Result,Ok,unwrap'
date: 2022-03-16 16:30:02
---
在其他传统语言的异常处理中,最常见的模式就是`try / catch`结构C/C++如此Java也是如此。在`try / catch`模式下我们可以自由的选择想要处理的异常甚至于可以直接忽略异常。但是在Rust中异常却变成了一个编程人无法回避的事情。<!-- more -->
!!! 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>`来说,可以如同下例中一样来处理函数的返回值。
```rust
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`类型结果中携带的值不同,决定是提取其中的成功值还是将错误值向上抛出。
例如之前定义的函数示例,如果要向上抛出错误值只需要这样做。
```rust
let stat_value = get_statistics(&student_score)?;
```
## 自定义错误类型
Rust这的错误实际上就是一个类型可以通过自定义一个结构体来定义。以下示例就定义了一个十分简单的错误类型。
```rust
#[derive(Debug, Clone)]
pub struct ParseError {
pub message: String,
pub errCode: u32,
}
```
这个自定义的错误类型在函数中可以通过以下方式返回使用。
```rust
fn some_function() -> Result<i64, ParseError> {
// ...
return Err(ParseError {
message: "出现意料之外的解析错误。".to_string(),
errCode: 20
});
}
```
但是仅仅这样定义异常还不能够让这个错误的行为更加接近标准库中的标准错误类型。如果想要达到接近标准错误类型的目的还需要让其实现一些指定的特型Trait
```rust
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所尝试的错误类型转换没有成功那么我们就又收获了一条类型错误。
要解决这个问题,第一个可以被想到的方法就是提供一条错误类型的转换路径。其实多种类型的错误在统一个函数中需要处理的时候,通常会将这些错误用一个枚举类型封装起来,并且实现`Display`、`Debug`、`Error`和`From<T>`这几个特型。这条路径手写起来比较枯燥繁琐,一般可以借助于`thiserror`包(用于库)或者`anyhow`包(用于bin程序)的辅助来提速实现。
```rust
#[derive(thiserror::Error, Debug)]
enum CustomError {
ParserError(ParseError),
#[error(transparent)]
IOError(#[from] std::io::Error),
}
```
其中`#[error()]`可以用于为错误指定一条描述信息,`#[from]`可以为错误实现`From`特型。这个错误类型在函数中只需要使用`?`操作符抛出错误即可使用。
如果不借助`thiserror`包的辅助,那么所需要编写的内容大概有以下这些。
```rust
#[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>`类型,所以就会产生一个十分简单的错误类型包装方案。
```rust
type CustomError = Box<std::error::Error>;
type CustomResult<T> = Result<T, CustomError>;
```
这样一来,程序在运行的时候,就会自动将任意错误类型转换为`CustomError`类型。除了可以利用`?`操作符进行自动类型转换以外,还可以为自定义错误类型实现`From`特型,来手动完成错误类型的转换。例如
```rust
impl<T> From<T> for CustomError
where
T: std::error::Error,
{
fn from(err: T) -> Self {
// 实际转换代码
}
}
```