深入淺出Rust Future - Part 2

譯自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。 我們還將從頭開始編寫未來的實現結構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章