美文网首页
Rust修行之Future篇-part2

Rust修行之Future篇-part2

作者: 黑天鹅学院 | 来源:发表于2020-01-08 14:29 被阅读0次

本文翻译自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返回的错误类型必须是相同的

下面我们来证实这一点。

我们有ErrorAErrorB两个类型,我们均为其实现error::Error,尽管编译器并没有这样的强制要求,但是我自认为这是个好习惯。

Errortrait同时要求实现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 ErrorBFromInto<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的实现。

相关文章

网友评论

      本文标题:Rust修行之Future篇-part2

      本文链接:https://www.haomeiwen.com/subject/mehsactx.html