Rust编程学习 - 问号运算符会return一个Result 类型,但是如何使用main函数中使用问号运算符
和Try trait一起设计的,还有 一个临时性的do catch语法。在使用do catch 的情况下,问号运算符就不是直接退出函数,而是退出do catch块。示例如下:
fn let file_double < P: AsRef < Path >> (file_path: P) {r: Result < i32,MyError >= docatch {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)};match r {Ok(n) = >println ! ("{}", n),Err(err) = >println ! ("Error:{:?}", err),}
}
目前使用这个语法需要打开#![feature(catch_expr)] 这个feature gate。而且语法 也是do catch{}这样的写法。这么做是为了避免导致代码不兼容的问题。以前的版本中, catch 这个单词不是关键字,可能就有用户使用了catch 这个名字作为标识符名字。如果 我们直接把这个单词升级为关键字,必然导致某些现存代码编译错误。所以引入关键字这种 事情,一定要通过edition 的版本更迭来完成。在Rust 2018 edition中,所有使用catch 作 为标识符的代码都会生成一个警告,但依然编译通过。再下一个edition 的时候,catch 就 可以提升为正式关键字了,到那时,就不需要do catch{}语法,而是直接使用catch {}了。(以后也可能选择try 作为关键字,目前还没有定论。)
大家可能又发现,如果使用问号运算符,主要逻辑那部分确实已经简化得非常干净,但 是其他部分的代码量又有了增长。我们需要定义新的错误类型,实现一些trait, 才能让它工 作起来。这部分能不能更简化一点呢?答案是肯定的。
其中一个方案是,使用trait object来替换enum。 实际上trait object和 enum 有很大的相 似性,它们都可以把一系列的类型统一成一个类型。恰好标准库内部给我们提供了一个trait, 来统一抽象所有的错误类型,它就是std::error::Error:
pub trait Error: Debug + Display {fn description( & self) - >&str;fn cause( & self) - >Option < &dyn Error > {…}
}
所有标准库里面定义的错误类型都已经实现了这个trait。所以,我们可以想象,错误类型 其实可以表示成 Box。下面用这种方式来精简一下file_double 这个例子:
use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
Box < dyn std: :error: :Error >> {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),}
}
上面这段代码也可以编译通过。因为std::io::Error 和 std::num::Parse-IntError 类型都实现了std::error::Error trait, 都可以转换为Box 这个 trait object。
统一用 trait object来接收所有错误是最简单的写法,但它也有缺点。最大的问题是,它 不方便向下转型。如果外面的调用者希望针对某些类型做特殊的错误处理,就很难办。除非 你不需要对任何错误类型做任何有区分的处理。这种写法适合一些简单的小工具。
使用enum 表达错误类型,可以最精确地表达错误信息。当然带来的一个后果是,被调 用者的enum 错误类型发生变化的时候(比如给enum 增加一个成员),会导致调用者那边编 译失败,这是由类型系统保证的。很多情况下,这其实是设计者愿意看到的结果,改变错误 类型本质上就是改变了API, 此事不该在调用者完全不知情的条件下默默进行。
当然,有些 情况下设计者的本意如果就是希望新增加一种错误类型但不影响下游用户的兼容性。这也是 有办法的,那就是最开始的版本就给这个enum 类型加上#[non_exhaustive]标签。这 样调用者那边的代码在做模式匹配的时候,无论如何都要写一条默认分支。以后给enum 新 加一个成员,就不会造成编译错误,调用者那边的流程会执行最开始的那条默认分支。具体 要不要使用这个标签,就取决于设计者的意图了。
main函数中使用问号运算符
新加入的问号运算符给main 函数带来了一个挑战。因为问号运算符会return 一 个 Result 类型,如果它所处的函数签名不是返回的Result 类型,一定会出现类型匹配错 误。而main 函数一开始的时候是定义成fn()->() 类型的,所以问号运算符不能在main 函 数中使用。这显然是一个问题,解决这个问题的办法就是——修改main 函数的签名类型。
我们希望:main 函数既可以返回unit 类型,不破坏以前的旧代码;又可以返回Result类型,支持使用问号运算符。所以,最简单的办法就是使用泛型,兼容这两种类型。Rust 在 标准库中引入了一个新的trait:
pub trait Termination {///Is called to get the representation of the value as status code./ 1 / This status code is returned to the operating system.fn report(self) - >i32;
}
main 函数的签名就对应地改成了fn<T:Termination>()->T。标准库为 Result 类型、()类型、 bool 类型以及发散类型!实现了这个trait。所以这些类型都可以作 为main 函数的返回类型了。
它是怎么启动起来的呢?是因为Runtime 库中有这样的一个函数,它调用了用户写的 main 函数:
# [lang = "start"] fn lang_start < T: ::termination: :Termination + 'static>
(main:fn()->T,argc:isize,argv:*const *const u8)->isize {
lang_start_internal(&move ||main().report(),argc,argv)
}
在最终的可执行代码中,程序刚启动的时候要先执行一些Runtime 自带的逻辑,然后才 会进入用户写的main 函数中去。
新的Failure 库
标准库中现存的Error trait有几个明显问题:
- description方法基本没什么用;
- 无法回溯,它没有记录一层层的错误传播的过程,不方便debug;
- Box 不是线程安全的。
Failure 这个库就是为了进一步优化错误处理而设计的。它主要包含三个部分。
- 新的failure::Fail trait, 是为了取代std::error::Error trait而设计的。它包含了更丰富的成员方法,且继承于Send +Sync,具备线程安全特性。
- 自 动derive机制,主要是让编译器帮用户写一些重复性的代码。
- failure::Error 结构体。所有其他实现了Fail trait的错误类型,都可以转换成 这个类型,而且它提供了向下转型的方法。
使用failure 来实现前面那个示例,代码如下:
# [macro_use] extern crate failure;use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;# [derive(Debug, Fail)] enum MyError {# [fail(display = "IO error {}·", _0)] Io(# [cause] std: :io: :Error),# [fail(display = "Parse error{}·", _0)] Parse(# [cause] 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),{}
现在社区里已经有一些库转向使用failure 做错误处理。它将来可能是Rust 生态系统中 主流的错误处理方式。
