V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
krircc
V2EX  ›  Rust

深入浅出 Rust Future - Part 2

  •  
  •   krircc · 2018-12-03 22:45:32 +08:00 · 3945 次点击
    这是一个创建于 2174 天前的主题,其中的信息可能已经有所发展或是发生改变。

    译自Rust futures: an uneducated, short and hopefully not boring tutorial - Part 2时间:2018-12-03,译者: motecshine, 简介:motecshine

    欢迎向 Rust 中文社区投稿,投稿地址 ,好文将在以下地方直接展示

    1. Rust 中文社区首页

    2. Rust 中文社区 Rust 文章栏目

    3. 知乎专栏Rust 语言

    4. sf.gg 专栏Rust 语言

    5. 微博Rustlang-cn

    Intro

    在这个系列的第一篇文章我们了解了如何使用Rust Future.但是只有我们彻底的了解Future并且操作得当才能发挥它真正的作用。这个系列的第二篇文章,我们将介绍如何避免Future里常见的陷阱。

    Error troubles

    我们将Future组织成一个很简单,只要通过Rust Future提供的and_then函数就可以了。但是在上一篇文章中我们使用了Box<Error> trait作为错误类型,绕过了编译器的检查。为什么我们没有使用更为详细的错误类型?原因很简单, 每个Future函数的错误返回都有可能不同.

    原则 1: 当我们将不同的Future组织成一个调用时,每个Future都应该返回相同的Error Type.

    让我们一起来证明一下这一点.

    我们有两个被叫做ErrorAErrorBError类型, 我们将会实现error::Error trait,尽管这并不是编译器必须让我们做的(但是这是一个好习惯[在我看来这应该算是一个最佳实践]),在我们实现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
        }
    }
    
    // Error B
    #[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
        }
    }
    

    我尽量用简单的方式去实现Error Trait,这样可以排除别的干扰来证明我的观点. 现在让我们在Future中使用ErrorAErrorB.

    fn fut_error_a() -> impl Future<Item = (), Error = ErrorA> {
        err(ErrorA {})
    }
    
    fn fut_error_b() -> impl Future<Item = (), Error = ErrorB> {
        err(ErrorB {})
    }
    

    现在让我们在main函数里调用它.

    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
    

    到现在为止还挺好的,让我们把ErrorAErrorB打包成一个调用链:

    let future = fut_error_a().and_then(|_| fut_error_b());
    

    我们先调用fut_error_a然后再调用fut_error_b,我们不用关心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

    原则 2: 当我们组织Future Chain时,第一个错误类型必须与最后一个future返回的错误类型一致.(When chaining futures, the first function error type must be the same as the chained one.)

    rustc已经非常明确的告诉我们了. 这个Future chain最终返回的是ErrorB所以我们第一个函数也应该返回ErrorB. 在上述代码我们返回了ErrorA, 所以导致编译失败.

    我们改如何处理这个问题?非常幸运的是, 我们可以使用Rust Future给我们提供的map_err方法. 在我们的示例中,我们想要把ErrorA转换成ErrorB,所以我们只需要在ErrorAErrorB之间调用这个函数就行了.

    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());
    

    我们最初的解决方式只适合成对的future, 并没有考虑其他的情况。所以在上面代码中我们不得不这么做: ErrorA => ErrorB => ErrorA.就像这样:

    let future = fut_error_a()
        .map_err(|_| ErrorB::default())
        .and_then(|_| fut_error_b())
        .map_err(|_| ErrorA::default())
        .and_then(|_| fut_error_a());
    

    看上去不那么优雅但是还是解决了多个Future的错误处理.

    "From" to the rescue

    简化上述代码的一种简单的方式就是利用std::covert::From. 当我们实现From, 这样编译器就可以自动的将一个结构软换为另一个结构.现在让我们实现From<ErrorA> for ErrorBFrom<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_err函数来代替map_err就好了。

    let future = fut_error_a()
       .from_err()
       .and_then(|_| fut_error_b())
       .from_err()
       .and_then(|_| fut_error_a());
    

    现在的代码仍然与错误转换混合, 但转换代码不再是内联的,而且代码可读性也提高了。Futrue Crate非常聪明:只有在错误的情况下才会调用from_err代码, 因此在不使用from_err时, 也不会在Runtime时产生额外的开销.

    ##Lifetimes

    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)
    }
    

    编译将会失败(rustc 1.23.0-nightly (2be4cc040 2017-11-01):

    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)
    }
    

    impl Future with lifetimes

    Future中如果有引用传参我们必须要显示的注释生命周期. 举个例子, 我们希望使用&s的值并且返回的是一个没有引用的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)))
    }
    

    上面的代码将会报错:

    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)))
    }
    

    现在你可以运行这段代码了:

    let retval = reactor
        .run(my_fut_ref_chained("str with lifetime"))
        .unwrap();
    println!("my_fut_ref_chained == {}", retval);
    

    Closing remarks

    在下一篇文章中,我们将介绍Reactor。 我们还将从头开始编写未来的实现结构。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   994 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 21:47 · PVG 05:47 · LAX 13:47 · JFK 16:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.