错误处理
在之前的返回值和错误处理 章节中,我们学习了几个重要的概念,例如
Result
用于返回结果处理,?
用于错误的传播,若大家对此还较为模糊,强烈建议回头温习下。
在本章节中一起来看看如何对 Result
( Option
) 做进一步的处理,以及如何定义自己的错误类型。
1. 组合器
在设计模式中,有一个组合器模式,相信有 Java
背景的同学对此并不陌生。
将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。–GoF
<<设计模式>>设计模式>
与组合器模式有所不同,在 Rust
中,组合器更多的是用于对返回结果的类型进行变换:例如使用
ok_or
将一个 Option
类型转换成
Result
类型。
下面我们来看看一些常见的组合器。
1.1 or() 和 and()
跟布尔关系的与/或很像,这两个方法会对两个表达式做逻辑组合,最终返回
Option
/ Result
。
or()
,表达式按照顺序求值,若任何一个表达式的结果是
Some
或 Ok
,则该值会立刻返回
and()
,若两个表达式的结果都是 Some
或
Ok
,则第二个表达式中的值被返回 。若任何一个的结果是
None
或 Err
,则立刻返回。
实际上,只要将布尔表达式的 true
/
false
,替换成 Some
/ None
或
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 fn main () { let s1 = Some ("some1" ); let s2 = Some ("some2" ); let n : Option <&str > = None ; let o1 : Result <&str , &str > = Ok ("ok1" ); let o2 : Result <&str , &str > = Ok ("ok2" ); let e1 : Result <&str , &str > = Err ("error1" ); let e2 : Result <&str , &str > = Err ("error2" ); assert_eq! (s1.or (s2), s1); assert_eq! (s1.or (n), s1); assert_eq! (n.or (s1), s1); assert_eq! (n.or (n), n); assert_eq! (o1.or (o2), o1); assert_eq! (o1.or (e1), o1); assert_eq! (e1.or (o1), o1); assert_eq! (e1.or (e2), e2); assert_eq! (s1.and (s2), s2); assert_eq! (s1.and (n), n); assert_eq! (n.and (s1), n); assert_eq! (n.and (n), n); assert_eq! (o1.and (o2), o2); assert_eq! (o1.and (e1), e1); assert_eq! (e1.and (o1), e1); assert_eq! (e1.and (e2), e1); }
除了 or
和 and
之外,Rust 还为我们提供了
xor
,但是它只能应用在 Option
上,其实想想也是这个理,如果能应用在 Result
上,那你又该如何对一个值和错误进行异或操作?
1.2 or_else() 和 and_then()
它们跟 or()
和 and()
类似,唯一的区别在于,它们的第二个表达式是一个闭包。
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 fn main () { let s1 = Some ("some1" ); let s2 = Some ("some2" ); let fn_some = || Some ("some2" ); let n : Option <&str > = None ; let fn_none = || None ; assert_eq! (s1.or_else (fn_some), s1); assert_eq! (s1.or_else (fn_none), s1); assert_eq! (n.or_else (fn_some), s2); assert_eq! (n.or_else (fn_none), None ); let o1 : Result <&str , &str > = Ok ("ok1" ); let o2 : Result <&str , &str > = Ok ("ok2" ); let fn_ok = |_| Ok ("ok2" ); let e1 : Result <&str , &str > = Err ("error1" ); let e2 : Result <&str , &str > = Err ("error2" ); let fn_err = |_| Err ("error2" ); assert_eq! (o1.or_else (fn_ok), o1); assert_eq! (o1.or_else (fn_err), o1); assert_eq! (e1.or_else (fn_ok), o2); assert_eq! (e1.or_else (fn_err), e2); }fn main () { let s1 = Some ("some1" ); let s2 = Some ("some2" ); let fn_some = |_| Some ("some2" ); let n : Option <&str > = None ; let fn_none = |_| None ; assert_eq! (s1.and_then (fn_some), s2); assert_eq! (s1.and_then (fn_none), n); assert_eq! (n.and_then (fn_some), n); assert_eq! (n.and_then (fn_none), n); let o1 : Result <&str , &str > = Ok ("ok1" ); let o2 : Result <&str , &str > = Ok ("ok2" ); let fn_ok = |_| Ok ("ok2" ); let e1 : Result <&str , &str > = Err ("error1" ); let e2 : Result <&str , &str > = Err ("error2" ); let fn_err = |_| Err ("error2" ); assert_eq! (o1.and_then (fn_ok), o2); assert_eq! (o1.and_then (fn_err), e2); assert_eq! (e1.and_then (fn_ok), e1); assert_eq! (e1.and_then (fn_err), e1); }
1.3 filter
filter
用于对 Option
进行过滤:
1 2 3 4 5 6 7 8 9 10 11 fn main () { let s1 = Some (3 ); let s2 = Some (6 ); let n = None ; let fn_is_even = |x: &i8 | x % 2 == 0 ; assert_eq! (s1.filter (fn_is_even), n); assert_eq! (s2.filter (fn_is_even), s2); assert_eq! (n.filter (fn_is_even), n); }
1.4 map() 和 map_err()
map
可以将 Some
或 Ok
中的值映射为另一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn main () { let s1 = Some ("abcde" ); let s2 = Some (5 ); let n1 : Option <&str > = None ; let n2 : Option <usize > = None ; let o1 : Result <&str , &str > = Ok ("abcde" ); let o2 : Result <usize , &str > = Ok (5 ); let e1 : Result <&str , &str > = Err ("abcde" ); let e2 : Result <usize , &str > = Err ("abcde" ); let fn_character_count = |s: &str | s.chars ().count (); assert_eq! (s1.map (fn_character_count), s2); assert_eq! (n1.map (fn_character_count), n2); assert_eq! (o1.map (fn_character_count), o2); assert_eq! (e1.map (fn_character_count), e2); }
但是如果你想要将 Err
中的值进行改变, map
就无能为力了,此时我们需要用 map_err
:
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let o1 : Result <&str , &str > = Ok ("abcde" ); let o2 : Result <&str , isize > = Ok ("abcde" ); let e1 : Result <&str , &str > = Err ("404" ); let e2 : Result <&str , isize > = Err (404 ); let fn_character_count = |s: &str | -> isize { s.parse ().unwrap () }; assert_eq! (o1.map_err (fn_character_count), o2); assert_eq! (e1.map_err (fn_character_count), e2); }
通过对 o1
的操作可以看出,与 map
面对
Err
时的短小类似, map_err
面对
Ok
时也是相当无力的。
1.5 map_or() 和 map_or_else()
map_or
在 map
的基础上提供了一个默认值:
1 2 3 4 5 6 7 8 9 10 fn main () { const V_DEFAULT: u32 = 1 ; let s : Result <u32 , ()> = Ok (10 ); let n : Option <u32 > = None ; let fn_closure = |v: u32 | v + 2 ; assert_eq! (s.map_or (V_DEFAULT, fn_closure), 12 ); assert_eq! (n.map_or (V_DEFAULT, fn_closure), V_DEFAULT); }
如上所示,当处理 None
的时候,V_DEFAULT
作为默认值被直接返回。
map_or_else
与 map_or
类似,但是它是通过一个闭包来提供默认值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main () { let s = Some (10 ); let n : Option <i8 > = None ; let fn_closure = |v: i8 | v + 2 ; let fn_default = || 1 ; assert_eq! (s.map_or_else (fn_default, fn_closure), 12 ); assert_eq! (n.map_or_else (fn_default, fn_closure), 1 ); let o = Ok (10 ); let e = Err (5 ); let fn_default_for_result = |v: i8 | v + 1 ; assert_eq! (o.map_or_else (fn_default_for_result, fn_closure), 12 ); assert_eq! (e.map_or_else (fn_default_for_result, fn_closure), 6 ); }
1.6 ok_or() and ok_or_else()
这两兄弟可以将 Option
类型转换为 Result
类型。其中 ok_or
接收一个默认的 Err
参数:
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { const ERR_DEFAULT: &str = "error message" ; let s = Some ("abcde" ); let n : Option <&str > = None ; let o : Result <&str , &str > = Ok ("abcde" ); let e : Result <&str , &str > = Err (ERR_DEFAULT); assert_eq! (s.ok_or (ERR_DEFAULT), o); assert_eq! (n.ok_or (ERR_DEFAULT), e); }
而 ok_or_else
接收一个闭包作为 Err
参数:
1 2 3 4 5 6 7 8 9 10 11 fn main () { let s = Some ("abcde" ); let n : Option <&str > = None ; let fn_err_message = || "error message" ; let o : Result <&str , &str > = Ok ("abcde" ); let e : Result <&str , &str > = Err ("error message" ); assert_eq! (s.ok_or_else (fn_err_message), o); assert_eq! (n.ok_or_else (fn_err_message), e); }
以上列出的只是常用的一部分,强烈建议大家看看标准库中有哪些可用的
API,在实际项目中,这些 API 将会非常有用: Option
和 Result 。
2. 自定义错误类型
虽然标准库定义了大量的错误类型,但是一个严谨的项目,光使用这些错误类型往往是不够的,例如我们可能会为暴露给用户的错误定义相应的类型。
为了帮助我们更好的定义错误,Rust
在标准库中提供了一些可复用的特征,例如 std::error::Error
特征:
1 2 3 4 5 use std::fmt::{Debug , Display};pub trait Error : Debug + Display { fn source (&self ) -> Option <&(Error + 'static )> { ... } }
当自定义类型实现该特征后,该类型就可以作为 Err
来使用,下面一起来看看。
实际上,自定义错误类型只需要实现 Debug
和
Display
特征即可,source
方法是可选的,而
Debug
特征往往也无需手动实现,可以直接通过
derive
来派生
2.1 最简单的错误
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 use std::fmt;#[derive(Debug)] struct AppError ;impl fmt ::Display for AppError { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { write! (f, "An Error Occurred, Please Try Again!" ) } }fn produce_error () -> Result <(), AppError> { Err (AppError) }fn main (){ match produce_error () { Err (e) => eprintln! ("{}" , e), _ => println! ("No error" ), } eprintln! ("{:?}" , produce_error ()); }
上面的例子很简单,我们定义了一个错误类型,当为它派生了
Debug
特征,同时手动实现了 Display
特征后,该错误类型就可以作为 Err
来使用了。
事实上,实现 Debug
和 Display
特征并不是作为 Err
使用的必要条件,大家可以把这两个特征实现和相应使用去除,然后看看代码会否报错。既然如此,我们为何要为自定义类型实现这两个特征呢?原因有二:
错误得打印输出后,才能有实际用处,而打印输出就需要实现这两个特征
可以将自定义错误转换成 Box<dyn std::error:Error>
特征对象,在后面的归一化不同错误类型 部分,我们会详细介绍
2.2 更详尽的错误
上一个例子中定义的错误非常简单,我们无法从错误中得到更多的信息,现在再来定义一个具有错误码和信息的错误:
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 use std::fmt;struct AppError { code: usize , message: String , }impl fmt ::Display for AppError { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self .code { 404 => "Sorry, Can not find the Page!" , _ => "Sorry, something is wrong! Please Try Again!" , }; write! (f, "{}" , err_msg) } }impl fmt ::Debug for AppError { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { write! ( f, "AppError {{ code: {}, message: {} }}" , self .code, self .message ) } }fn produce_error () -> Result <(), AppError> { Err (AppError { code: 404 , message: String ::from ("Page not found" ), }) }fn main () { match produce_error () { Err (e) => eprintln! ("{}" , e), _ => println! ("No error" ), } eprintln! ("{:?}" , produce_error ()); eprintln! ("{:#?}" , produce_error ()); }
在本例中,我们除了增加了错误码和消息外,还手动实现了
Debug
特征,原因在于,我们希望能自定义 Debug
的输出内容,而不是使用派生后系统提供的默认输出形式。
2.3 错误转换 From
特征
标准库、三方库、本地库,各有各的精彩,各也有各的错误。那么问题就来了,我们该如何将其它的错误类型转换成自定义的错误类型?总不能神鬼牛魔,同台共舞吧。。
好在 Rust 为我们提供了 std::convert::From
特征:
1 2 3 pub trait From <T>: Sized { fn from (_: T) -> Self ; }
事实上,该特征在之前的 ?
操作符 章节中就有所介绍。
大家都使用过 String::from
函数吧?它可以通过
&str
来创建一个 String
,其实该函数就是
From
特征提供的
下面一起来看看如何为自定义类型实现 From
特征:
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 std::fs::File;use std::io;#[derive(Debug)] struct AppError { kind: String , message: String , }impl From <io::Error> for AppError { fn from (error: io::Error) -> Self { AppError { kind: String ::from ("io" ), message: error.to_string (), } } }fn main () -> Result <(), AppError> { let _file = File::open ("nonexistent_file.txt" )?; Ok (()) } Error: AppError { kind: "io" , message: "No such file or directory (os error 2)" }
上面的代码中除了实现 From
外,还有一点特别重要,那就是
?
可以将错误进行隐式的强制转换:File::open
返回的是 std::io::Error
,
我们并没有进行任何显式的转换,它就能自动变成 AppError
,这就是 ?
的强大之处!
上面的例子只有一个标准库错误,再来看看多个不同的错误转换成
AppError
的实现:
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 use std::fs::File;use std::io::{self , Read};use std::num;#[derive(Debug)] struct AppError { kind: String , message: String , }impl From <io::Error> for AppError { fn from (error: io::Error) -> Self { AppError { kind: String ::from ("io" ), message: error.to_string (), } } }impl From <num::ParseIntError> for AppError { fn from (error: num::ParseIntError) -> Self { AppError { kind: String ::from ("parse" ), message: error.to_string (), } } }fn main () -> Result <(), AppError> { let mut file = File::open ("hello_world.txt" )?; let mut content = String ::new (); file.read_to_string (&mut content)?; let _number : usize ; _number = content.parse ()?; Ok (()) } Error: AppError { kind: "io" , message: "No such file or directory (os error 2)" } Error: AppError { kind: "io" , message: "Permission denied (os error 13)" } Error: AppError { kind: "parse" , message: "invalid digit found in string" }
3. 归一化不同的错误类型
至此,关于 Rust
的错误处理大家已经了若指掌了,下面再来看看一些实战中的问题。
在实际项目中,我们往往会为不同的错误定义不同的类型,这样做非常好,但是如果你要在一个函数中返回不同的错误呢?例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 use std::fs::read_to_string;fn main () -> Result <(), std::io::Error> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String , std::io::Error> { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }
上面的代码会报错,原因在于 render
函数中的两个
?
返回的实际上是不同的错误:env::var()
返回的是 std::env::VarError
,而 read_to_string
返回的是 std::io::Error
。
为了满足 render
函数的签名,我们就需要将
env::VarError
和 io::Error
归一化为同一种错误类型。要实现这个目的有三种方式:
使用特征对象 Box<dyn Error>
自定义错误类型
使用 thiserror
下面依次来看看相关的解决方式。
3.1 Box
大家还记得我们之前提到的 std::error::Error
特征吧,当时有说:自定义类型实现 Debug + Display
特征的主要原因就是为了能转换成 Error
的特征对象,而特征对象恰恰是在同一个地方使用不同类型的关键:
1 2 3 4 5 6 7 8 9 10 11 12 13 use std::fs::read_to_string;use std::error::Error;fn main () -> Result <(), Box <dyn Error>> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String , Box <dyn Error>> { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }
这个方法很简单,在绝大多数场景中,性能也非常够用,但是有一个问题:Result
实际上不会限制错误的类型,也就是一个类型就算不实现 Error
特征,它依然可以在 Result<T, E>
中作为 E
来使用,此时这种特征对象的解决方案就无能为力了。
3.2 自定义错误类型
与特征对象相比,自定义错误类型麻烦归麻烦,但是它非常灵活,因此也不具有上面的类似限制:
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 use std::fs::read_to_string;fn main () -> Result <(), MyError> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String , MyError> { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }#[derive(Debug)] enum MyError { EnvironmentVariableNotFound, IOError (std::io::Error), }impl From <std::env::VarError> for MyError { fn from (_: std::env::VarError) -> Self { Self ::EnvironmentVariableNotFound } }impl From <std::io::Error> for MyError { fn from (value: std::io::Error) -> Self { Self ::IOError (value) } }impl std ::error::Error for MyError {}impl std ::fmt::Display for MyError { fn fmt (&self , f: &mut std::fmt::Formatter<'_ >) -> std::fmt::Result { match self { MyError::EnvironmentVariableNotFound => write! (f, "Environment variable not found" ), MyError::IOError (err) => write! (f, "IO Error: {}" , err.to_string ()), } } }
上面代码中有一行值得注意:impl std::error::Error for MyError {}
,只有为自定义错误类型实现 Error
特征后,才能转换成相应的特征对象。
不得不说,真是啰嗦啊。因此在能用特征对象的时候,建议大家还是使用特征对象,无论如何,代码可读性还是很重要的!
上面的第二种方式灵活归灵活,啰嗦也是真啰嗦,好在 Rust
的社区为我们提供了 thiserror
解决方案,下面一起来看看该如何简化 Rust 中的错误处理。
4. 简化错误处理
对于开发者而言,错误处理是代码中打交道最多的部分之一,因此选择一把趁手的武器也很重要,它可以帮助我们节省大量的时间和精力,好钢应该用在代码逻辑而不是冗长的错误处理上。
4.1 thiserror
thiserror
可以帮助我们简化上面的第二种解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::fs::read_to_string;fn main () -> Result <(), MyError> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String , MyError> { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }#[derive(thiserror::Error, Debug)] enum MyError { #[error("Environment variable not found" )] EnvironmentVariableNotFound (#[from] std::env::VarError), #[error(transparent)] IOError (#[from] std::io::Error), }
如上所示,只要简单写写注释,就可以实现错误处理了,惊不惊喜?
4.2 error-chain
error-chain
也是简单好用的库,可惜不再维护了,但是我觉得它依然可以在合适的地方大放光彩,值得大家去了解下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use std::fs::read_to_string; error_chain::error_chain! { foreign_links { EnvironmentVariableNotFound (::std::env::VarError); IOError (::std::io::Error); } }fn main () -> Result <()> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String > { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }
喏,简单吧?使用 error-chain
的宏你可以获得:Error
结构体,错误类型
ErrorKind
枚举 以及一个自定义的 Result
类型。
4.3 anyhow
anyhow
和 thiserror
是同一个作者开发的,这里是作者关于
anyhow
和 thiserror
的原话:
如果你想要设计自己的错误类型,同时给调用者提供具体的信息时,就使用
thiserror
,例如当你在开发一个三方库代码时。如果你只想要简单,就使用
anyhow
,例如在自己的应用服务中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::fs::read_to_string;use anyhow::Result ;fn main () -> Result <()> { let html = render ()?; println! ("{}" , html); Ok (()) }fn render () -> Result <String > { let file = std::env::var ("MARKDOWN" )?; let source = read_to_string (file)?; Ok (source) }
关于如何选用 thiserror
和 anyhow
只需要遵循一个原则即可:是否关注自定义错误消息 ,关注则使用
thiserror
(常见业务代码),否则使用
anyhow
(编写第三方库代码)。