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()
}
}
|