组合错误类型
利用代数类型系统做错误处理的另外一大好处是可组合性(composability)。比如 Result 类型有这样的一系列成员方法:
js
fn map < U,
F > (self, op: F) - >Result < U,
E > where F: FnOnce(T) - >U fn map_err < F,
0 > (self, op: 0) - >Result < T,
F > where O: FnOnce(E) - >F fn and < U > (self, res: Result < U, E > ) - >Result < U,
E > fn and_then < U,
F > (self, op: F) - >Result < U,
E > where F: FnOnce(T) - >Result < U,
E > fn or < F > (self, res: Result < T, F > ) - >Result < T,
F > fn or_else < F,
0 > (self, op: 0) - >Result < T,
F > where O: FnOnce(E) - >Result < T,
F >
这些方法的签名稍微有点复杂,涉及许多泛型参数。它们之间的区别也就表现在方法签 名中。我们可以用下面的方式去掉语法干扰之后,来阅读函数签名,从而理解这些方法之间 的区别:

通过这个表格的对比,我们可以很容易看出它们之间的区别。比如map和and_ then 的主要区别是闭包参数:map的参数是做的T->U的转换,而and_then的参数是 T->Result 的转换。 Option 类型也有类似的对应的方法,读者可以自己建一个表格,对 比一下这些方法签名之间的区别。
下面用一个示例演示一下这些组合函数的用法:
js
use std: :env;
fn double_arg(mut argv: env: :Args) - >Result < i32,
String > {
argv.nth(1).ok_or("Please give at least one argument".to_owned()).and_then( | arg | arg.parse: :<i32 > ().map_err(lerr | err.to_string())).map( | n | 2 * n)
}
fn main() {
match double_arg(env: :args()) {
Ok(n) = >println ! ("{}", n),
Err(err) = >println ! ("Error:{}", err),
}
}
问号运算符
Result类型的组合调用功能很强大,但是它有一个缺点,就是经常会发生嵌套层次太 多的情况,不利于可读性。比如下面这个示例:
js
use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
String > {
File: :open(file_path)
.map_err( | err | err.to_string()).and_then( | mut file | {
let mut contents = String: :new();
file.read_to_string( & mut contents).map_err( | err | err.to_string()).map(I_I contents)
}).and_then( | contents | {
contents.trim()·parse: :<i32 > ().map_err( | err | err.to_string())
}).map( | n | 2 * n)
fn main() {
match file_double("foobar") {
Ok(n) = >println ! ("{}", n),
Err(err) = >println ! ("Error:{}", err),
}
}
这说明我们还有继续改进的空间。为了方便用户,Rust 设计组在前面这套系统的基础上, 又加入了一个问号运算符,用来简化源代码。这个问号运算符完全是建立在前面这套错误处 理机制上的语法糖。
问号运算符意思是,如果结果是Err,则提前返回,否则继续执行。使用问号运算符,我们可以把 file_double 函数简化成这样:
js
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
String > {
let mut file = File: :open(file_path).map_err( | e | e.to_string()) ? ;
let mut contents = String: :new();
file.read_to_string( & mut contents).map_err( | err | err.to_string()) ? ;
let n = contents.trim()·parse: :<i32 > ().map_err( | err | err.to_string()) ? ;
Ok(2 * n)
}
这里依然有不少的map_err 调用,主要原因是返回类型限制成了Result<i32, String> 。 如果改一下返回类型,代码还能继续精简。
因为这段代码总共有两种错误:
- 一种是io 错误,用std::io::Error 表示
- 另外一种是字符串转整数错误,用std::num::ParseIntError表示。
我们要把这两种类型统一起 来,所以使用了一个自定义的enum 类型,这样map_err 方法调用就可以省略了。我们再补 充这两种错误类型到自定义错误类型之间的类型转换,问题就解决了。完整源码如下所示:
js
use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;
# [derive(Debug)] enum MyError {
Io(std: :io: :Error),
Parse(std: :num: :ParseIntError),
}
impl From < std: :io: :Error >
for MyError {
fn from(error: std: :io: :Error) - >Self {
MyError: :Io(error)
}
}
impl From < std: :num: :ParseIntError >
for MyError {
fn from(error: std: :num: :ParseIntError) - >Self {
MyError: :Parse(error)
}
}
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
MyError > {
let mut file = File: :open(file_path) ? ;
let mut contents = String: :new();
file.read_to_string( & mut contents) ? ;
let n = contents.trim().parse: :<i32 > () ? ;
Ok(2 * n)
}
fn main() {
match file_double("foobar") {
Ok(n) = >println ! ("{}", n),
Err(err) = >println ! ("Error:{:?}", err),
}
}
这样一来,这个file_double 函数就精简太多了。它只需管理正常逻辑,对于可能出 错的分支,直接一个问号操作符提前返回,错误处理和正常逻辑互不干扰,清晰易读。
下面继续讲解一下问号运算符背后做了什么事情。跟其他很多运算符一样,问号运算符 也对应着标准库中的一个trait std::ops::Try。它的定义如下:
js
trait Try {
type Ok;
type Error;
fn into_result(self) - >Result < Self: :Ok,
Self: :Error > ;
fn from_error(v: Self: :Error) - >Self;
fn from_ok(v: Self: :Ok) - >Self;
}
match Try: :into_result(expr) {
Ok(V) = >V,
//here,the 'return presumes that there is
Err(e) = >
return Try: :from_error(From: :from(e)),
}
哪些类型支持这个问号表达式呢?标准库中已经为Option、Result两个类型impl了 这个trait:
js
impl < T > ops: :Try
for Option < T > {
type Ok = T;
type Error = NoneError;
fn into_result(self) - >Result < T,
NoneError > {
self.ok_or(NoneError)
}
fn from_ok(V: T) - >Self {
Some(V)
}
fn from_error(_: NoneError) - >Self {
None
}
}
impl < T,
E > ops: :Try
for Result < T,
E > {
type Ok = T;
type Error = E;
fn into_result(self) - >Self {
self
}
fn from_ok(v: T) - >Self {
Ok(V)
}
fn from_error(v: E) - >Self {
Err(V)
}
把这些综合起来,我们就能理解对于Result类型,执行问号运算符做了什么了。其实 就是碰到Err的话,调用From trait 做个类型转换,然后中断当前逻辑提前返回。