Rust 异步工作组很高兴地宣布,我们在实现特征中使用 `async fn` 的目标方面取得了重大进展。Rust 1.75 将在下周发布稳定版,其中将包含对特征中的 `-> impl Trait` 语法和 `async fn` 的支持。
这是一个重要的里程碑,我们知道许多用户都迫不及待地想在自己的代码中尝试这些功能。但是,我们仍然缺少许多用户需要的某些重要功能。请继续阅读以了解有关何时以及如何使用已稳定功能的建议。
正在稳定什么
自从 Rust 1.26 中稳定了 RFC #1522 以来,Rust 允许用户将 `impl Trait` 作为函数的返回类型(通常称为“RPIT”)。这意味着函数返回“实现 `Trait` 的某种类型”。这通常用于返回闭包、迭代器和其他难以或无法显式编写的类型。
/// Given a list of players, return an iterator
/// over their names.
fn player_names(
players: &[Player]
) -> impl Iterator<Item = &String> {
players
.iter()
.map(|p| &p.name)
}
从 Rust 1.75 开始,您可以在特征定义和特征实现中使用 **返回位置 `impl Trait` 在特征中**(RPITIT)。例如,您可以使用它来编写返回迭代器的特征方法
trait Container {
fn items(&self) -> impl Iterator<Item = Widget>;
}
impl Container for MyContainer {
fn items(&self) -> impl Iterator<Item = Widget> {
self.items.iter().cloned()
}
}
那么这一切与异步函数有什么关系呢?好吧,异步函数只是返回 `-> impl Future` 的函数的“语法糖”。由于这些现在在特征中是允许的,**我们也允许您编写使用 `async fn` 的特征**。
trait HttpService {
async fn fetch(&self, url: Url) -> HtmlBody;
// ^^^^^^^^ desugars to:
// fn fetch(&self, url: Url) -> impl Future<Output = HtmlBody>;
}
差距在哪里
公共特征中的 `-> impl Trait`
由于用户无法对返回类型添加额外的约束,因此仍然不建议在 **公共** 特征和 API 中普遍使用 `-> impl Trait`。例如,无法以泛型的方式编写以下函数
fn print_in_reverse(container: impl Container) {
for item in container.items().rev() {
// ERROR: ^^^
// the trait `DoubleEndedIterator`
// is not implemented for
// `impl Iterator<Item = Widget>`
eprintln!("{item}");
}
}
即使某些实现可能返回实现 `DoubleEndedIterator` 的迭代器,但泛型代码也无法利用这一点,除非定义另一个特征。将来,我们计划为此添加解决方案。目前,最好在内部特征中使用 `-> impl Trait`,或者当您确信用户不需要额外的约束时。否则,您应该考虑使用关联类型。1
公共特征中的 `async fn`
由于 `async fn` 会反糖化为 `-> impl Future`,因此相同的限制适用。实际上,如果您今天在公共特征中使用裸 `async fn`,您将看到一个警告。
warning: use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified
--> src/lib.rs:7:5
|
7 | async fn fetch(&self, url: Url) -> HtmlBody;
| ^^^^^
|
help: you can desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`, but these cannot be relaxed without a breaking API change
|
7 - async fn fetch(&self, url: Url) -> HtmlBody;
7 + fn fetch(&self, url: Url) -> impl std::future::Future<Output = HtmlBody> + Send;
|
对异步用户特别感兴趣的是返回的 Future 上的 `Send` 约束。由于用户无法稍后添加约束,因此错误消息表明您作为特征作者需要做出选择:您希望您的特征与多线程、工作窃取执行器一起使用吗?
值得庆幸的是,我们有一个解决方案,允许您今天在公共特征中使用 `async fn`!我们建议使用 `trait_variant::make` 过程宏来让您的用户选择。此过程宏是 trait-variant
crate 的一部分,由 rust-lang 组织发布。使用 `cargo add trait-variant` 将其添加到您的项目中,然后像这样使用它
#[trait_variant::make(HttpService: Send)]
pub trait LocalHttpService {
async fn fetch(&self, url: Url) -> HtmlBody;
}
这将创建您特征的 **两个** 版本:用于单线程执行器的 `LocalHttpService` 和用于多线程工作窃取执行器的 `HttpService`。由于我们预计后者将更常被使用,因此在本例中它具有较短的名称。它具有额外的 Send 约束
pub trait HttpService: Send {
fn fetch(
&self,
url: Url,
) -> impl Future<Output = HtmlBody> + Send;
}
此宏适用于异步,因为 `impl Future` 很少需要除了 Send 之外的额外约束,因此我们可以为用户设置成功条件。请参阅下面的常见问题解答,了解需要此功能的示例。
动态调度
使用 `-> impl Trait` 和 `async fn` 的特征不是对象安全的,这意味着它们不支持动态调度。我们计划在即将发布的 `trait-variant` crate 版本中提供支持动态调度的实用程序。
我们希望如何在未来改进
将来,我们希望允许用户向 `impl Trait` 返回类型添加自己的约束,这将使它们更普遍地有用。它还将支持更高级的 `async fn` 使用方式。语法可能如下所示
trait HttpService = LocalHttpService<fetch(): Send> + Send;
由于这些别名不需要特征作者的任何支持,因此从技术上讲,它将使异步特征的 Send 变体变得不必要。但是,这些变体仍然是用户的便利功能,因此我们预计大多数 crate 将继续提供它们。
当然,异步工作组的目标并不止于在特征中使用 `async fn`。我们希望继续在此基础上构建功能,以支持更可靠、更复杂的异步 Rust 使用方式,并且我们打算在新的一年发布更全面的路线图。
常见问题解答
在特征中使用 `-> impl Trait` 可以吗?
对于私有特征,您可以自由地使用 `-> impl Trait`。对于公共特征,现在最好避免使用它们,除非您可以预料到用户可能需要的全部约束(在这种情况下,您可以使用 `#[trait_variant::make]`,就像我们对异步所做的那样)。我们预计将来会解除此限制。
我是否应该继续使用 `#[async_trait]` 宏?
您可能需要继续使用 async-trait 的原因有两个
- 您希望支持低于 1.75 的 Rust 版本。
- 您希望进行动态调度。
如上所述,我们希望在 `trait-variant` crate 的未来版本中启用动态调度。
在特征中使用 `async fn` 可以吗?有什么限制?
假设您不需要出于上述任何原因使用 `#[async_trait]`,那么在特征中使用常规的 `async fn` 完全没问题。只需记住,如果您希望支持多线程运行时,请使用 `#[trait_variant::make]`。
最大的限制是,类型必须始终决定是实现特征的 Send 版本还是非 Send 版本。它不能根据其泛型来 *有条件地* 实现 Send 版本。例如,这可能出现在 中间件 模式中,例如 `RequestLimitingService<T>`,如果 `T: HttpService`,则它是 HttpService。
为什么我需要 `#[trait_variant::make]` 和 `Send` 约束?
在简单的情况下,您可能会发现您的特征似乎可以与多线程执行器正常工作。但是,有些模式根本无法正常工作。考虑以下情况
fn spawn_task(service: impl HttpService + 'static) {
tokio::spawn(async move {
let url = Url::from("https://www.rust-lang.net.cn");
let _body = service.fetch(url).await;
});
}
如果我们的特征没有 Send 约束,这将无法编译,并出现错误:“future 无法安全地在线程之间发送”。通过创建具有 Send 约束的特征变体,您可以避免将用户陷入这种陷阱。
请注意,如果您的特征不是公共的,您将不会看到警告,因为如果您遇到此问题,您始终可以在以后添加 Send 约束。
我可以混合使用 async fn 和 impl trait 吗?
是的,您可以在特征和实现中自由地在 `async fn` 和 `-> impl Future` 语法之间切换。即使其中一种形式具有 `Send` 约束,也是如此。3 这使得由 `trait_variant` 创建的特征更易于使用。
trait HttpService: Send {
fn fetch(&self, url: Url)
-> impl Future<Output = HtmlBody> + Send;
}
impl HttpService for MyService {
async fn fetch(&self, url: Url) -> HtmlBody {
// This works, as long as `do_fetch(): Send`!
self.client.do_fetch(url).await.into_body()
}
}
为什么这些签名不使用 `impl Future + '_`?
对于特征中的 `-> impl Trait`,我们早期采用了 2024 捕获规则。这意味着您今天经常看到的 `+ '_` 在特征中是不必要的,因为返回类型被假定为已经捕获了输入生命周期。在 2024 版中,此规则将应用于所有函数签名。有关更多信息,请参阅链接的 RFC。
为什么我在用 `-> impl Trait` 实现特征时收到“refine”警告?
如果您的实现签名包含比特征本身更详细的信息,您将 收到警告
pub trait Foo {
fn foo(self) -> impl Debug;
}
impl Foo for u32 {
fn foo(self) -> String {
// ^^^^^^
// warning: impl trait in impl method signature does not match trait method signature
self.to_string()
}
}
原因是您可能泄露了比您意图更多的实现细节。例如,以下代码应该编译吗?
fn main() {
// Did the implementer mean to allow
// use of `Display`, or only `Debug` as
// the trait says?
println!("{}", 32.foo());
}
由于 精炼特征实现,它确实编译了,但编译器要求您在实现上使用 `#[allow(refining_impl_trait)]` 来确认您要精炼特征接口的意图。
结论
异步工作组很高兴在 2023 年结束时宣布我们今年的主要目标已完成!感谢所有在设计、实现和稳定性讨论中提供帮助的人。也感谢多年来使用异步 Rust 的用户提供了宝贵的反馈。我们期待着看到您构建的内容,并在未来几年继续提供改进。
-
请注意,关联类型只能在类型可命名的情况下使用。一旦
impl_trait_in_assoc_type
稳定,此限制将被解除。 ↩ -
请注意,在那篇博文中,我们最初表示将在发布特征中的 `async fn` 之前解决 `Send` 约束问题,但我们决定将其从范围中删除,而是发布 `trait-variant` crate。 ↩
-
这是因为自动特征泄露,它允许自动特征的知识从签名未指定它们的项目中“泄露”。 ↩