Rust修行之Future篇-part2

本文翻译自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的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容