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

深入浅出 Rust Future - Part 1

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

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

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

    1. Rust 中文社区首页

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

    3. 知乎专栏Rust 语言

    4. sf.gg 专栏Rust 语言

    Intro

    如果你是一个程序员并且也喜欢 Rust 这门语言, 那么你应该经常在社区听到讨论Future 这个库的声音, 一些很优秀的Rust Crates都使用了Future 所以我们也应该对它有足够的了解并且使用它. 但是大多数程序员很难理解Future到底是怎么工作的, 当然有官方 Crichton's tutorial这样的教程, 虽然很完善, 但我还是很难理解并把它付诸实践.

    我猜测我并不是唯一一个遇到这样问题的程序员, 所以我将分享我自己的最佳实践, 希望这能帮助你理解这个话题.

    Futures in a nutshell

    Future 是一个不会立即执行的特殊functions. 他会在将来执行(这也是他被命名为future的原因).我们有很多理由让future functions来替代std functions,例如: 优雅性能可组合性.future的缺点也很明显: 很难用代码去实现. 当你不知道何时会执行某个函数时, 你又怎么能理解他们之间的因果关系呢?

    处于这个原因,Rust 会试图帮助我们这些菜鸟程序员去理解和使用future这个特性。

    Rust's futures

    Rust 的futures 总是一个Results: 这意味着你必须同时指定期待的返回值和备用的错误类型。 让我们先简单的实现一个方法,然后把它改造成future. 我们设计的这个方法返回值是 u32 或者是一个 被Box包围着的Error trait, 代码如下所示:

    fn my_fn() -> Result<u32, Box<Error>> { 
        Ok(100) 
    } 
    

    这段代码很简单,看起来并没有涉及到future. 接下来让我们看看下面的代码:

    fn my_fut() -> impl Future<Item = u32, Error = Box<Error>> { 
        ok(100) 
    } 
    

    注意这两段代码不同的地方:

    1. 返回的类型不再是Result而是一个impl Future. Rust Nightly版本是允许我们返回一个future的。

    2. 第二个函数返回值的参量Item = u32, Error = Box<Error>较第一个函数来看更加详细明确。

    为了能让第二段代码工作 你需要使用拥有conservative_impl_trait特性的nightly版本。当然,如果不嫌麻烦,你可以使用boxed trait来替代。

    另请注意第一个函数返回值使用的是大写的Ok(100)。 在Result函数中,我们使用大写的Ok枚举,而future我们使用小写的 ok 方法.

    规则: 在 Rustfuture中使用小写返回方法ok(100).

    好了现在我们改造完毕了,但是我们该怎样执行第二个我们改造好的方法?标准方法我们可以直接调用,但是这里需要注意的是地一个方法返回值是一个Result, 所以我们需要使用unwrap()来获取我们期待的值。

    let retval = my_fn().unwrap(); 
    println!("{:?}", retval); 
    

    由于future在实际执行之前返回(或者更准确的说, 返回的是我们将来要执行的代码), 我们需要一种途径去执行future。为此我们使用Reactor。我们只需要创建一个Reactor并且调用他的run方法就可以执行future. 就像下面的代码:

    let mut reactor = Core::new().unwrap(); 
    let retval = reactor.run(my_fut()).unwrap(); 
    println!("{:?}", retval); 
    

    注意这里我们unwrap的是run方法,而不是my_fut. 看起来真的很简单。

    Chaining

    future一个很重要的特性就是能够把其他的future组织起来形成一个chain. 举个栗子:

    你邀请你的父母一起吃晚饭通过 email. 你在电脑前等待他们的回复 父母同意与你一起吃晚饭(或者因为一些原因拒绝了)。

    Chaining就是这样的,让我们看一个简单的例子:

    fn my_fn_squared(i: u32) -> Result<u32, Box<Error>> { 
         Ok(i * i) 
    } 
    
    fn my_fut_squared(i: u32) -> impl Future<Item = u32, Error = Box<Error>> { 
        ok(i * i) 
    } 
    

    现在我们可以使用下面的方式去调用这两个函数:

    let retval = my_fn().unwrap(); 
    println!("{:?}", retval); 
    let retval2 = my_fn_squared(retval).unwrap(); 
    println!("{:?}", retval2); 
    

    当然我们也可以模拟Reactor来执行相同的代码:

    let mut reactor = Core::new().unwrap(); 
    let retval = reactor.run(my_fut()).unwrap(); 
    println!("{:?}", retval); 
    let retval2 = reactor.run(my_fut_squared(retval)).unwrap(); 
    println!("{:?}", retval2); 
    

    但还有更好的方法,在 Rust 中future也是一个trait他有很多种方法(这里我们会介绍些),其中一个名为and_then的方法,在语义上完全符合我们最后写的代码片段。但是没有显式的执行Reactor Run, 让我们一起来看看下面的代码:

    let chained_future = my_fut().and_then(|retval| my_fut_squared(retval));
    let retval2 = reactor.run(chained_future).unwrap(); 
    println!("{:?}", retval2); 
    

    让我们看看第一行:创建一个被叫做chained_futurefuture, 它把my_futmu_fut_squared``future串联了起来。 这里让人难以理解的部分是: 我们如何将上一个future的结果传递给下一个future?

    在 Rust 中我们可以通过闭包来捕获外部变量来传递future的值。 可以这样想:

    1. 调度并且执行my_fut()
    2. my_fut()执行完毕后,创建一个retval变量并且将my_fut()的返回值存到其中。
    3. 现在将retval作为my_fn_squared(i: u32)的参数传递进去,并且调度执行my_fn_squared
    4. 把上面一些列的操作打包成一个名为chained_future的调用链。

    第二行代码,与之前的相同: 我们调用Reactor run(), 要求执行chained_future并给出结果。 当然我们可以通过这种方式将无数个future打包成一个chain, 不要去担心性能问题, 因为future chainzero cost.

    RUST borrow checked可能让你的future chain 写起来不是那么的轻松,所以你可以尝试move你的参数变量.

    Mixing futures and plain functions

    你也可以使用普通的函数来做future chain, 这很有用, 因为不是每个功能都需要使用future. 此外, 你也有可能希望调用外部你无法控制的函数。 如果函数没有返回 Result,你只需在闭包中添加函数调用即可。 例如,如果我们有这个普通函数:

    fn fn_plain(i: u32) -> u32 { 
        i - 50  
    } 
    
    let chained_future = my_fut().and_then(|retval| { 
        let retval2 = fn_plain(retval); 
        my_fut_squared(retval2) 
    }); 
    let retval3 = reactor.run(chained_future).unwrap(); 
    println!("{:?}", retval3); 
    

    如果你的函数返回Result则有更好的办法。我们一起来尝试将my_fn_squared(i: u32) -> Result<u32, Box<Error>方法打包进future chain

    在这里由于返回值是Result所以你无法调用and_then, 但是future有一个方法done()可以将Result转换为impl Future.这意味着我们可以将普通的函数通过done方法把它包装成一个future.

    let chained_future = my_fut().and_then(|retval| { 
        done(my_fn_squared(retval)).and_then(|retval2| my_fut_squared(retval2)) 
    }); 
    let retval3 = reactor.run(chained_future).unwrap(); 
    println!("{:?}", retval3); 
    

    注意第二:done(my_fn_squared(retval))允许我们在链式调用的原因是:我们将普通函数通过done方法转换成一个impl Future. 现在我们不使用done方法试试:

    let chained_future = my_fut().and_then(|retval| {
        my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2)) 
    }); 
    let retval3 = reactor.run(chained_future).unwrap(); 
    println!("{:?}", retval3); 
    

    编译不通过!

    Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2) 
    error[E0308]: mismatched types 
    --> src/main.rs:136:50 | 136 | my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2)) | ^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found anonymized type | = note: expected type `std::result::Result<_, std::boxed::Box<std::error::Error>>` found type `impl futures::Future` 
    error: aborting due to previous error 
    error: Could not compile `tst_fut2`. 
    

    expected type std::result::Result<_, std::boxed::Box<std::error::Error>> found type impl futures::Future,这个错误有点让人困惑. 我们将会在第二部分讨论它。

    Generics

    最后但并非最不重要的, futuregeneric(这是啥玩意儿啊)一起工作不需要任何黑魔法.

    fn fut_generic_own<A>(a1: A, a2: A) -> impl Future<Item = A, Error = Box<Error>> where A: std::cmp::PartialOrd, { 
        if a1 < a2 { ok(a1) } else { ok(a2) } 
    } 
    

    这个函数返回的是 a1 与 a2 之间的较小的值。但是即便我们很确定这个函数没有错误也需要给出Error,此外,返回值在这种情况下是小写的ok(原因是函数, 而不是enmu)

    现在我们调用这个future:

    let future = fut_generic_own("Sampdoria", "Juventus"); 
    let retval = reactor.run(future).unwrap(); 
    println!("fut_generic_own == {}", retval); 
    

    阅读到现在你可能对future应该有所了解了, 在这边文章里你可能注意到我没有使用&, 并且仅使用函数自身的值。这是因为使用impl Future,生命周期的行为并不相同,我将在下一篇文章中解释如何使用它们。在下一篇文章中我们也会讨论如何在future chain处理错误和使用 await!()宏。

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