深入淺出Rust Future
本文譯自Rust futures: an uneducated, short and hopefully not boring tutorial - Part 1,時間:2018-12-02,譯者: motecshine, 簡介:motecshine
歡迎向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)
}
注意這兩段程式碼不同的地方:
-
返回的型別不再是
Result
而是一個impl Future
.Rust Nightly
版本是允許我們返回一個future
的。 -
第二個函式返回值的參量
Item = u32, Error = Box<Error>
較第一個函式來看更加詳細明確。
為了能讓第二段程式碼工作 你需要使用擁有
conservative_impl_trait
特性的nightly
版本。當然,如果不嫌麻煩,你可以使用boxed trait
來替代。
另請注意第一個函式返回值使用的是大寫的Ok(100)
。 在Result
函式中,我們使用大寫的Ok
列舉,而future
我們使用小寫的ok方法.
規則: 在Rust
future
中使用小寫返回方法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_future
的future
, 它把my_fut
與mu_fut_squared``future
串聯了起來。 這裡讓人難以理解的部分是: 我們如何將上一個future
的結果傳遞給下一個future
?
在Rust中我們可以通過閉包來捕獲外部變數來傳遞
future
的值。 可以這樣想:
- 排程並且執行
my_fut()
- 當
my_fut()
執行完畢後,建立一個retval
變數並且將my_fut()
的返回值存到其中。 - 現在將
retval
作為my_fn_squared(i: u32)
的引數傳遞進去,並且排程執行my_fn_squared
。 - 把上面一些列的操作打包成一個名為
chained_future
的呼叫鏈。
第二行程式碼,與之前的相同: 我們呼叫Reactor run()
, 要求執行chained_future
並給出結果。 當然我們可以通過這種方式將無數個future
打包成一個chain
, 不要去擔心效能問題, 因為future chain
是 zero 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
最後但並非最不重要的, future
與 generic
(這是啥玩意兒啊)一起工作不需要任何黑魔法.
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!()巨集。