2.6.4 async 与返回 impl Trait

译文 · 基于 Learning Rust

async 与返回 impl Trait

原文链接: https://quinedot.github.io/rust-learning/m-rpitit-alikes.html

关于 async fn 和返回 impl Trait 可以说的很多,这里无法全部涵盖。但特别需要注意的是,它们以几乎不可见的方式向函数签名引入借用关系。

关于 -> impl Trait 与隐式捕获

你可以使用 impl Trait 作为返回类型,返回调用者知道满足该 trait 的东西,而无需实际命名类型。这也称为"返回位置的 impl Trait",或 RPIT。

当你使用 -> impl Trait 时,生命周期(和类型泛型)可能几乎不可见地流入返回类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# #![deny(elided_lifetimes_in_paths)]
# use either::Either;
# use std::fs::File;
# use std::io::{self, BufRead, BufReader};
# use std::iter;
fn example(s: &str) -> impl Iterator<Item = Result<String, io::Error>> {
    match File::open(s) {
        Ok(file) => Either::Left(BufReader::new(file).lines()),
        Err(e) => Either::Right(iter::once(Err(e))),
    }
}

这个签名实际上是以下写法的语法糖:

1
2
//                          vvvvvvvvv
fn example(s: &str) -> impl use<'_> + Iterator<Item = Result<String, io::Error>> {

这称为"捕获"泛型生命周期。在本例中,这意味着调用者会将返回值视为包含我们传入的 &str(对迭代器的使用会让 *s 保持借用)。然而,我们实际上并没有这样做!因此这可能导致意外的借用检查器错误,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# #![deny(elided_lifetimes_in_paths)]
# use either::Either;
# use std::fs::File;
# use std::io::{self, BufRead, BufReader};
# use std::iter;
# fn example(s: &str) -> impl Iterator<Item = Result<String, io::Error>> {
#     match File::open(s) {
#         Ok(file) => Either::Left(BufReader::new(file).lines()),
#         Err(e) => Either::Right(iter::once(Err(e))),
#     }
# }
fn main() {
    let local = String::from("filename.txt");
    let iter = example(&local);
    let _move_of_local = local; // 报错,因为 `local` 仍被借用……
    for _ in iter {}            // ……就好像 `&local` 进入了 `iter`
}

返回值让 local 保持借用,因为函数 API 告诉编译器要强制执行这一点。捕获生命周期是 API 契约的一部分。

默认情况下,所有泛型都会被捕获(所有生命周期和所有类型)。但我们可以手动写出自己的 use 子句,只捕获我们实际需要的生命周期。(到目前为止,你始终必须捕获所有类型泛型。)

在本例中,我们不需要任何生命周期——我们实际上没有在返回值中使用 s——因此修改如下:

1
2
-fn example(s: &str) -> impl Iterator<Item = Result<String, io::Error>> {
+fn example(s: &str) -> impl use<> + Iterator<Item = Result<String, io::Error>> {

现在这可以编译了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# #![deny(elided_lifetimes_in_paths)]
# use either::Either;
# use std::fs::File;
# use std::io::{self, BufRead, BufReader};
# use std::iter;
fn example(s: &str) -> impl use<> + Iterator<Item = Result<String, io::Error>> {
    // ...
#     match File::open(s) {
#         Ok(file) => Either::Left(BufReader::new(file).lines()),
#         Err(e) => Either::Right(iter::once(Err(e))),
#     }
}

fn main() {
    let local = String::from("filename.txt");
    let iter = example(&local);
    let _move_of_local = local;
    for _ in iter {}
}

据我所知,没有 lint 要求显式写出 use<..> 子句。相反,我建议养成将 -> impl 视为可能发生借用关系的信号的习惯,类似于你看到 -> &_ 时的反应。

展开查看一些历史说明。

在旧版 edition 中,trait 外的 RPIT 默认不捕获生命周期。 在那些 edition 上,有人可能使用 + use<..> 来捕获更多生命周期而不是更少! (但理想情况下,你应该使用最新的稳定版 Rust edition。)

如 edition 指南所述,你也可能看到 -> impl Trait + 'lifetime 而不是 + use<'lifetime>。这种模式在 use<..> 子句创建之前就已经稳定,因此变得有些常见,但 use<..> 通常是正确的选择。

async fn 捕获

async fn 在底层使用 RPIT 能力。因此,async 函数的返回类型会捕获其所有泛型参数,包括任何生命周期。所以这里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
async fn example(v: &mut Vec<String>) -> String {
    "Hi :-)".to_string()
}

// 概念上等价于:
// fn example(v: &mut Vec<String>) -> impl use<'_> + Future<Output = String> {
//     async move {
//         // 始终捕获一切!
//         let v = v;
//         "Hi :-)".to_string()
//     }
// }

async fn 返回的 future 隐式重借用 v 输入,并"携带"相同的生命周期,就像我们之前看到的其他例子一样。

因此,你应该将 async fn 与 -> impl 类似地看待:一个标志,表明返回类型可能持有来自输入的借用。

如果你遇到捕获不必要的情况,可以将 async fn 改写为返回 impl Trait 的(普通)fn:

1
2
3
4
5
6
7
8
// 注意空的 `use<>`……
fn example(v: &mut Vec<String>) -> impl use<> + Future<Output = String> {
    let _ignore = v;
    // ……并且不要在 `async` 块内移动或以其他方式使用该借用
    async {
        "Hi :-)".to_string()
    }
}
最后修改 August 23, 2026: 更新 (499855b16)