本文翻译自Rust futures: an uneducated, short and hopefully not boring tutorial - Part 2
介绍
在前一篇文章中,我们介绍了怎样在rust中使用Future,如果掌握了正确的方法,Future将是很强大并且易于使用的特性。在这一篇文章中,我们将主要关注一些常见的陷阱,以及如何避免踩坑。
错误问题
在上一篇文章中,我们简单的通过and_then
函数将Future组织成了一条链,为了绕过编译器检查,我们使用Box<Error>
trait作为错误类型。使用Box包裹错误一定程度上增加了复杂度,为什么不使用更加精确的错误类型呢?原因在于,当把不同的Future级联成链时,每一个Future都必须返回相同的错误类型。
rust级联准则:
在进行Future级联时,所有的Future返回的错误类型必须是相同的
下面我们来证实这一点。
我们有ErrorA
和ErrorB
两个类型,我们均为其实现error::Error
,尽管编译器并没有这样的强制要求,但是我自认为这是个好习惯。
Error
trait同时要求实现std::fmt::Display
,所以我们定义如下:
#[derive(Debug, Default)]
pub struct ErrorA {}
impl fmt::Display for ErrorA {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ErrorA!")
}
}
impl error::Error for ErrorA {
fn description(&self) -> &str {
"Description for ErrorA"
}
fn cause(&self) -> Option<&error::Error> {
None
}
}
ErrorB
是类似的:
#[derive(Debug, Default)]
pub struct ErrorB {}
impl fmt::Display for ErrorB {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ErrorB!")
}
}
impl error::Error for ErrorB {
fn description(&self) -> &str {
"Description for ErrorB"
}
fn cause(&self) -> Option<&error::Error> {
None
}
}
为了证实我们的观点,我尽可能简化了实现方法。现在我们在Future中使用这些结构体。
fn fut_error_a() -> impl Future<Item = (), Error = ErrorA> {
err(ErrorA {})
}
fn fut_error_b() -> impl Future<Item = (), Error = ErrorB> {
err(ErrorB {})
}
接下来,我们执行这些Future
let retval = reactor.run(fut_error_a()).unwrap_err();
println!("fut_error_a == {:?}", retval);
let retval = reactor.run(fut_error_b()).unwrap_err();
println!("fut_error_b == {:?}", retval);
返回结果与我们的预期一致
fut_error_a == ErrorA
fut_error_b == ErrorB
到目前为止,并没有出现什么惊喜,我们开始来级联:
let future = fut_error_a().and_then(|_| fut_error_b());
这个Future链执行的过程很简单,先调用fut_error_a
,然后再调用fut_error_b
。由于我们不关心fut_error_a
的返回值,所以我们用下划线来设置闭包参数,表示丢弃fut_error_a
的返回值。
可以用更复杂的术语来表达这一行,我们将impl Future<Item=(), Error=ErrorA>
和 impl Future<Item=(), Error=ErrorB>
打包成了一个调用链。
我们尝试编译后,得到了如下的结果:
Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error[E0271]: type mismatch resolving `<impl futures::Future as futures::IntoFuture>::Error == errors::ErrorA`
--> src/main.rs:166:32
|
166 | let future = fut_error_a().and_then(|_| fut_error_b());
| ^^^^^^^^ expected struct `errors::ErrorB`, found struct `errors::ErrorA`
|
= note: expected type `errors::ErrorB`
found type `errors::ErrorA`
报错提示是很直白的,编译器期待一个ErrorB
的类型,但是却提供了一个ErrorA
.
用简单的说法就是:
在进行Future级联的时候,第一个函数的错误类型,必须与级联的下一个函数相同。
这个原则也是编译器想告诉我们的. 第二个函数返回的是ErrorB
类型,所以第一个函数必须也返回ErrorB
类型。然而,实际上第一个函数返回类型是ErrorA
,所以编译失败。
应该怎样处理这个问题呢,rust提供了一个map_err
方法,利用这个方法我们可以转换错误类型。在我们的例子中,我们需要把ErrorA
转换为ErrorB
,转换过程如下:
let future = fut_error_a()
.map_err(|e| {
println!("mapping {:?} into ErrorB", e);
ErrorB::default()
})
.and_then(|_| fut_error_b());
let retval = reactor.run(future).unwrap_err();
println!("error chain == {:?}", retval);
重新编译运行,我们得到了相同的结果:
mapping ErrorA into ErrorB
error chain == ErrorB
我们将这个例子复杂化,假设我们想连接ErrorA
,然后是ErrorB
,然后再连接ErrorA
:
let future = fut_error_a()
.and_then(|_| fut_error_b())
.and_then(|_| fut_error_a());
为了处理错误类型,我们转换错误类型:
let future = fut_error_a()
.map_err(|_| ErrorB::default())
.and_then(|_| fut_error_b())
.map_err(|_| ErrorA::default())
.and_then(|_| fut_error_a());
尽管这种方法比较繁琐,但是能解决问题。
通过实现From简化代码
简化上述代码的一种简单的方式就是利用std::covert::From
trait。当我们为一个struct实现了From后,编译器可以自动的将一个结构软换为另一个结构。现在让我们实现From<ErrorA> for ErrorB
和FromInto<ErrorB> for ErrorA
。
impl From<ErrorB> for ErrorA {
fn from(e: ErrorB) -> ErrorA {
ErrorA::default()
}
}
impl From<ErrorA> for ErrorB {
fn from(e: ErrorA) -> ErrorB {
ErrorB::default()
}
}
实现了From后,我们可以使用from_err()
取代map_err()
函数,而rust编译器会自动进行类型的转换。
let future = fut_error_a()
.from_err()
.and_then(|_| fut_error_b())
.from_err()
.and_then(|_| fut_error_a());
现在的代码看起来简洁了一些,但是仍然混杂了代码错误处理, 但至少转换代码不再是内联的,代码可读性显著提高。Futrue Crate非常聪明,只有在错误的情况下才会调用from_err
代码, 因此不会在Runtime时产生额外的开销。
生命周期
rust签名功能是指显式的标注引用的的生命周期,大多数情况下,rust允许我们使用缺省的生命周期。我们来看一个实际的例子,编写一个带字符串引用参数的函数,返回原字符串的引用。
fn my_fn_ref<'a>(s: &'a str) -> Result<&'a str, Box<Error>> {
Ok(s)
}
注意代码中的<'a>
, 通过这个标记,我们显式的声明了一个生命周期。然后,我们声明了一个引用形参s: &'a str
, 这个参数必须在'a
生命周期有效的情况下使用。使用Result<&' str, Box<Error>>
,我们告诉rust,我们的返回值将包含一个字符串引用,并且只有当’a
有效时,该字符串引用才是有效的。换句话说,传递的字符串引用和返回的对象必须具有相同的生命周期。这些设定导致我们的语法非常冗长,以至于rust允许我们使用缺省的生命周期。 所以我们可以这样重写函数:
fn my_fn_ref(s: &str) -> Result<&str, Box<Error>> {
Ok(s)
}
在这个函数中,没有显示的生命周期标记,尽管生命周期的概念仍然存在,这样的写法简化了代码实现。
在目前的Future中,不能使用缺省生命周期处理引用, 让我们来尝试用Future的方式重写这个函数:
fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
ok(s)
}
这样的写法将会编译失败:
Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error: internal compiler error: /checkout/src/librustc_typeck/check/mod.rs:633: escaping regions in predicate Obligation(predicate=Binder(ProjectionPredicate(ProjectionTy { substs: Slice([_]), item_def_id: DefId { krate: CrateNum(15), index: DefIndex(0:330) => futures[59aa]::future[0]::Future[0]::Item[0] } }, &str)),depth=0)
--> src/main.rs:39:36
|
39 | fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: the compiler unexpectedly panicked. this is a bug.
note: we would appreciate a bug report: https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md#bug-reports
note: rustc 1.23.0-nightly (2be4cc040 2017-11-01) running on x86_64-unknown-linux-gnu
thread 'rustc' panicked at 'Box<Any>', /checkout/src/librustc_errors/lib.rs:450:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
当然也有解决方法,我们只要显示声明一个有效的生命周期就行了:
fn my_fut_ref<'a>(s: &'a str) -> impl Future<Item = &'a str, Error = Box<Error>> {
ok(s)
}
实现包含生命周期的Future
在Future中,如果用引用传参,我们必须要显式的标记生命周期. 举个例子, 在Future中接受一个&str
参数,返回一个String,我们必须显示的标记生命周期。
fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}
由于返回的Future没有被限制在'a
的生命周期内,这段代码将会报错:
error[E0564]: only named lifetimes are allowed in `impl Trait`, but `` was found in the type `futures::AndThen<impl futures::Future, futures::FutureResult<std::string::String, std::boxed::Box<std::error::Error + 'static>>, [closure@src/main.rs:44:28: 44:64]>`
--> src/main.rs:43:42
|
43 | fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为了解决这个错误,我们必须为impl Future
追加一个'a
生命周期:
fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> + 'a {
my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}
执行这个Future:
let retval = reactor
.run(my_fut_ref_chained("str with lifetime"))
.unwrap();
println!("my_fut_ref_chained == {}", retval);
我们得到的预期结果如下:
my_fut_ref_chained == received == str with lifetime
尾声
在下一篇文章中,我们将介绍Reactor,同时,我们还将从零开始编写一个Future的实现。