Rust中的错误处理

如果你使用过其他编程语言,那么就会知道Rust中的错误处理是完全不同的方法。像Java,JS,Python等你常常会使用throw处理异常,以及return成功的值。但在Rust中,你会返回给调用者一个Result

Result<T, E>是一个拥有两个值的枚举类型,其中Ok(T)用来返回成功值,Err(E)用来返回错误值

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

返回错误而不是抛出错误,这是一种编程习惯的转变。所以如果你是Rust的初学者,可能刚开始学习起来觉得很麻烦,因为这需要你用去思考在不同的场景,应该使用什么样的方法去处理错误。

在这篇博客中会有错误处理的一些范式,以及它们在Rust中是如何体现的:

  • 忽略错误
  • 直接结束程序
  • 使用默认值处理
  • 传递错误
  • 传递多个错误
  • 模式匹配Boxed错误
  • 使用库 or 应用
  • 创建自定义错误
  • 传递自定义错误
  • 模式匹配自定义错误

忽略错误(unwrap())

(如果出现错误会触发panic,让该线程退出)

最简单的处理方法就是直接忽略这个错误,这听起来是不太好的想法,但是可以在以下情况使用:

  • 刚刚开始编写代码,不想浪费太多时间在错误处理上。
  • 坚定确信当前的情况下,错误一定不会发生。
1
2
3
4
5
6
use std::fs;

fn main() {
let content = fs::read_to_string("./Cargo.toml").unwrap();
println!("{}", content)
}

即使知道文件会存在,但是编译器也无法知道。因此,使用unwrap()关键字让编译器信任,并返回其中的值。如果read_to_string()函数返回一个Ok(),unwrap将获取Ok()的内容并将其分配给content变量。如果它返回一个错误,那么程序会陷入panic这回让当前程序线程退出。

需要注意的是在许多Rust示例代码中使用unwrap来跳过错误处理,但是这样做主要是为了方便,不应该在实际开发中使用。

结束程序(expect())

有些错误无法处理或从中恢复。在这些情况下,最好直接终止程序。 让使用与上面相同的例子——正在读取一个文件,肯定会看到它。想象一下,对于这个程序来说,这个文件绝对重要,没有它就无法正常工作。如果由于某种原因,该文件不存在,那么最好应该直接终止该程序。

可以像之前一样使用unwrap或者使用expect,它和unwrap差不多,唯一不同的是添加了额外的错误信息。

1
2
3
4
5
6
use std::fs;

fn main() {
let content = fs::read_to_string("./Cargo.toml").expect("Can't read Cargo.toml");
println!("{}", content)
}

了解关于:panic!

使用默认值(unwrap_or())

在某些情况下,可以通过返回默认值来处理错误。

例如正在编写一个服务器,它监听的端口可以使用环境变量进行配置。如果没有设置环境变量,则访问该值将导致错误。但可以通过返回默认值来轻松处理这个问题。

1
2
3
4
5
6
use std::env;

fn main() {
let port = env::var("PORT").unwrap_or("3000".to_string());
println!("{}", port);
}

在这里,使用在这里,使用了一种称为unwrap_orunwrap变体,它允许提供默认值。

了解关于: unwrap_or_else, unwrap_or_default

传递错误给调用者

当没有足够的context来处理错误时,可以将错误冒泡(向上传播)到调用者函数。下面是一个精心设计的示例,它使用Web服务获取当前年份:

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
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n {}", e),
}
}

fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let result = reqwest::blocking::get(url);

let response = match result {
Ok(res) => res,
Err(err) => return Err(err),
};

let body = response.json::<HashMap<String, i32>>();

let json = match body {
Ok(json) => json,
Err(err) => return Err(err),
};

let date = json["years"].to_string();

Ok(date)
}

这块在get_current_date中有两个函数调用(get和json)会返回Result值。因为get_current_date并没有返回错误时要做什么的上下文,所以使用模式匹配将错误传回main.

有时候在使用模式匹配去处理,多个嵌套的错误处理可能会让代码看起来非常混乱,所以可以引入?来重写上述代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n {}", e),
}
}

fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let date = res["years"].to_string();

Ok(date)
}

这看起来简洁明了。其中?操作符类似于unwrap但是在遇到Error时并不会产生panic,而是会将错误返回给调用者函数。需要记住的一件事情是只有在函数返回OptionResult类型时才能使用?操作符。

Rust语言中Option和Result两种类型的使用_rust option result-CSDN博客

注意Option和Result:

1
2
3
4
5
6
7
8
9
10
pub enum Option<T> {
None,
Some(T),
}

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

其中Option更多用于一个返回值不确定是否存在的情况下(可以理解为其他语言中的NULL)。

而Result更多用于会出现错误进行捕捉的场景。

所以会看到很多函数后面的返回值(可以return,也可以直接None/Some/Ok/Err)是这样的,给出各自的例子:

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
31
32
33
34
35
36
37
38
39
40
41
42
// 使用Option处理可能存在或不存在的值
fn find_element_index(arr: &[i32], target: i32) -> Option<usize> {
for (index, &value) in arr.iter().enumerate() {
if value == target {
return Some(index);
}
}
None
}

// 使用Result处理可能的错误情况
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Cannot divide by zero") //这里会返回Result中的Err,内容是这个字符串字面量
} else {
Ok(a / b) //这里会返回Result中的Ok,内容就是实际除法得到的结果
}
}
/*这里 &'static str 表示错误信息是一个指向程序生命周期为整个程序的字符串字面量的引用。使用 &'static str 而不是 String 可以避免在堆上分配内存,从而提高效率。
&'static str 表示一个指向字符串字面量的引用,其生命周期是 'static。这意味着这个字符串字面量在程序的整个生命周期内都是有效的。
为什么使用 'static 生命周期?
避免动态内存分配:字符串字面量存储在程序的只读数据段中,使用 'static 生命周期可以避免在堆上为错误消息分配额外的内存。这在处理大量错误时尤其重要,因为它可以减少内存消耗。
简化错误处理:使用静态字符串可以简化错误处理逻辑,因为编译器可以保证这些字符串在整个程序生命周期内都是有效的,无需担心生命周期问题。*/

fn main() {
let numbers = [1, 2, 3, 4, 5];

// 使用Option
let index = find_element_index(&numbers, 3);
match index {
Some(i) => println!("Element found at index: {}", i),
None => println!("Element not found"),
}

// 使用Result
match divide(10, 2) {
Ok(result) => println!("Result of division: {}", result),
Err(error) => println!("Error: {}", error),
}
divide(10,2).unwrap();
divide(10,2).expect("defined by yourself");
}

传递多个错误给调用者

在之前的例子中,getjson函数返回了一个reqwest::Error错误。但是如果已经有了一个在调用其他的函数时返回的错误类型,那么应该怎么处理呢?

让通过返回格式化的日期而不是年份来扩展上一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ use chrono::NaiveDate;
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n {}", e),
}
}

fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
- let date = res["years"].to_string();
+ let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+ let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

上述的代码是不能编译的,由于parse_from_str返回了一个chrono::format::ParseError错误而不是reqwest::Error

可以使用Box关键字来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  use chrono::NaiveDate;
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n {}", e),
}
}

- fn get_current_date() -> Result<String, reqwest::Error> {
+ fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

当想要发返回多个不同的错误时,返回一个特征对象 Box<dyn std::error::Error>是一种便利的处理方法。

了解更多关于: anyhow, eyre

模式匹配Boxed错误

目前为止,只在main中打印错误,而不是真正地去处理它们。如果想处理和恢复Box错误,需要“downcast”它们:

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
 use chrono::NaiveDate;
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
- Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( \n {}", e),
+ Err(e) => {
+ eprintln!("Oh noes, we don't know which era we're in! :(");
+ if let Some(err) = e.downcast_ref::<reqwest::Error>() {
+ eprintln!("Request Error: {}", err)
+ } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
+ eprintln!("Parse Error: {}", err)
+ }
+ }
}
}

fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

需要注意的是,必须知道get_current_date的实现细节(其中包含的不同错误),才能够在main中对其进行downcast。

了解更多关于: downcast, downcast_mut

库 vs 应用

如前面所述,使用Box带来的问题是:如果想要处理底层错误,必须了解函数的实现细节。当以Box< dyn std::error::Error>形式返回某个内容时,具体的类型信息将会丢失。为了以不同方式处理不同的错误,需要将它们向下转换为某个具体类型,这种转换可能会在运行时失败。

然而,脱离上下文,谈论好坏并不是很有用。一个很好的经验法则是思考当前正在编写的代码是一个“Application”还是“Library”:

Application应用

  • 您正在编写的代码将由最终用户使用。
  • 大多数由应用程序代码生成的错误不会被处理,而是记录或报告给用户。
  • 可以使用box错误。

Library库

  • 您正在编写的代码将被其他代码使用。一个“库”可以是开源crate,内部library等。
  • 错误是库的API的一部分,因此库的使用者知道应该期望并从中恢复哪些错误。
  • 库中的错误通常由用户处理,因此它们需要结构化且易于执行exhaustive match
  • 如果您返回Box错误,那么库的使用者需要知道由代码、依赖项等创建的错误类型!
  • 可以返回自定义错误,而不是Box错误。

创建自定义错误

对于library代码,可以将所有错误转换为自定义的错误类型,并返回它,而不是使用特征对象box。在的例子中,目前由两个错误reqwest::Errorchrono::format::ParseError。可以将它们分别转换为MyCustomError::HttpErrorMyCustomError::ParseError

首先要创建一个enum来装的两个错误变量

1
2
3
4
5
6
// error.rs

pub enum MyCustomError {
HttpError,
ParseError,
}

Errortrait规定实现必须DebugDisplay traits:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// error.rs

use std::fmt;

#[derive(Debug)]
pub enum MyCustomError {
HttpError,
ParseError,
}

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

impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyCustomError::HttpError => write!(f, "HTTP Error"),
MyCustomError::ParseError => write!(f, "Parse Error"),
}
}
}

就这样创建的自定义错误类型,这是一个非常简单的例子,但是没有包含太多关于错误的信息。但这应该足以作为创建更复杂、更现实的自定义错误的起点。下面是一些常见开发中的自定义错误处理的例子:ripgrep, reqwest, csv and serde_json

还有: thiserror, snafu

传递自定义错误

看看使用自定义错误类型后对于之前程序的修改

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
  // main.rs

+ mod error;

use chrono::NaiveDate;
+ use error::MyCustomError;
use std::collections::HashMap;

fn main() {
// skipped, will get back later
}

- fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
+ fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
- let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
+ let res = reqwest::blocking::get(url)
+ .map_err(|_| MyCustomError::HttpError)?
+ .json::<HashMap<String, i32>>()
+ .map_err(|_| MyCustomError::HttpError)?;

let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
- let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
+ .map_err(|_| MyCustomError::ParseError)?;
let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

注意到使用了map_err将一个错误类型转换为另外的错误类型.

但是可以看到太多的verbose作为结果,的函数充斥着太多map_err调用。可以实现From trait,这样就会完成自动错误类型转换。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  // error.rs

use std::fmt;

#[derive(Debug)]
pub enum MyCustomError {
HttpError,
ParseError,
}

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

impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyCustomError::HttpError => write!(f, "HTTP Error"),
MyCustomError::ParseError => write!(f, "Parse Error"),
}
}
}

+ impl From<reqwest::Error> for MyCustomError {
+ fn from(_: reqwest::Error) -> Self {
+ MyCustomError::HttpError
+ }
+ }

+ impl From<chrono::format::ParseError> for MyCustomError {
+ fn from(_: chrono::format::ParseError) -> Self {
+ MyCustomError::ParseError
+ }
+ }
// main.rs

mod error;

use chrono::NaiveDate;
use error::MyCustomError;
use std::collections::HashMap;

fn main() {
// skipped, will get back later
}

fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
- let res = reqwest::blocking::get(url)
- .map_err(|_| MyCustomError::HttpError)?
- .json::<HashMap<String, i32>>()
- .map_err(|_| MyCustomError::HttpError)?;
+ let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
- let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
- .map_err(|_| MyCustomError::ParseError)?;
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

在移除map_err后代码变得更加整洁了。

然而,Fromtrait并不是一种能减少使用map_err的魔法。在上面的例子中,将类型转换从get_current_data函数内部移动到From<X> for MyCustomError实现.那么如果一个错误没有在MyCustomError出现过,就不能使用From trait,从而只能使用map_err

模式匹配自定义错误

一直忽略了main的变动,现在要看看如何去处理自定义错误类型的模式匹配问题。

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
31
32
33
34
35
36
  // main.rs

mod error;

use chrono::NaiveDate;
use error::MyCustomError;
use std::collections::HashMap;

fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => {
eprintln!("Oh noes, we don't know which era we're in! :(");
- if let Some(err) = e.downcast_ref::<reqwest::Error>() {
- eprintln!("Request Error: {}", err)
- } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
- eprintln!("Parse Error: {}", err)
- }
+ match e {
+ MyCustomError::HttpError => eprintln!("Request Error: {}", e),
+ MyCustomError::ParseError => eprintln!("Parse Error: {}", e),
+ }
}
}
}

fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();

Ok(date)
}

请注意,与Boxed错误不同,实际上可以直接匹配MyCustomError enum中的变量。