Rust 异步工作组很高兴地宣布,我们在实现 trait 中使用 async fn
的目标方面取得了重大进展。Rust 1.75 将于下周发布稳定版,它将包含对 trait 中的 -> 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 开始,您可以在 trait 中的返回位置 impl Trait
(RPITIT) 定义和 trait 实现中使用它。例如,您可以使用它来编写一个返回迭代器的 trait 方法
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
的函数的“语法糖”。由于现在在 trait 中允许这样做,因此 我们也允许您编写使用 async fn
的 trait。
trait HttpService {
async fn fetch(&self, url: Url) -> HtmlBody;
// ^^^^^^^^ desugars to:
// fn fetch(&self, url: Url) -> impl Future<Output = HtmlBody>;
}
差距在哪里
-> impl Trait
公共 trait 中的 出于用户无法对返回类型施加额外约束的原因,仍然不鼓励在 公共 trait 和 API 中普遍使用 -> impl Trait
。例如,无法以对 Container
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
的迭代器,也没有办法让泛型代码利用这一点,而无需定义另一个 trait。将来我们计划为此添加一个解决方案。目前,-> impl Trait
最好用于内部 trait,或者当您确信您的用户不需要额外的约束时使用。否则,您应该考虑使用关联类型。1
async fn
公共 trait 中的 由于 async fn
会被解糖为 -> impl Future
,因此也适用相同的限制。事实上,如果您今天在公共 trait 中使用裸 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
约束。由于用户以后无法添加约束,因此错误消息表明作为 trait 作者,您需要做出选择:您希望您的 trait 与多线程、工作窃取执行器一起使用吗?
值得庆幸的是,我们有一个解决方案,允许今天在公共 trait 中使用 async fn
!我们建议使用 trait_variant::make
过程宏,让您的用户选择。这个过程宏是 trait-variant
crate 的一部分,由 rust-lang org 发布。使用 cargo add trait-variant
将其添加到您的项目中,然后像这样使用它
#[trait_variant::make(HttpService: Send)]
pub trait LocalHttpService {
async fn fetch(&self, url: Url) -> HtmlBody;
}
这将创建您的 trait 的两个版本:用于单线程执行器的 LocalHttpService
和用于多线程工作窃取执行器的 HttpService
。由于我们预计后者会更常用,因此在本示例中它的名称较短。它具有额外的 Send 约束
pub trait HttpService: Send {
fn fetch(
&self,
url: Url,
) -> impl Future<Output = HtmlBody> + Send;
}
此宏适用于异步,因为 impl Future
很少需要除了 Send 之外的额外约束,因此我们可以让用户获得成功。请参阅下面的 FAQ,了解何时需要它的示例。
动态分发
使用 -> impl Trait
和 async fn
的 trait 不是对象安全的,这意味着它们缺乏对动态分发的支持。我们计划在即将发布的 trait-variant
crate 版本中提供支持动态分发的实用程序。
我们希望如何在未来改进
未来,我们希望允许用户向 impl Trait
返回类型添加自己的约束,这将使其更普遍地有用。它还将启用 async fn
的更高级用法。语法可能如下所示
trait HttpService = LocalHttpService<fetch(): Send> + Send;
由于这些别名不需要 trait 作者的任何支持,因此从技术上讲,它将使异步 trait 的 Send 变体变得不必要。但是,这些变体对于用户来说仍然是一个方便的工具,因此我们预计大多数 crate 将继续提供它们。
当然,异步工作组的目标不仅仅是在 trait 中使用 async fn
。我们希望继续在其基础上构建功能,从而实现更可靠和复杂的异步 Rust 用法,并且我们打算在新的一年发布更广泛的路线图。
常见问题
-> impl Trait
可以吗?
在 trait 中使用 对于私有 trait,您可以自由使用 -> impl Trait
。对于公共 trait,最好暂时避免使用它们,除非您可以预测您的用户可能想要的所有约束(在这种情况下,您可以使用 #[trait_variant::make]
,就像我们对异步所做的那样)。我们预计将来会解除此限制。
#[async_trait]
宏吗?
我仍然应该使用 您可能需要继续使用 async-trait 的几个原因:
- 您希望支持早于 1.75 的 Rust 版本。
- 您需要动态分发。
如上所述,我们希望在未来版本的 trait-variant
crate 中启用动态分发。
async fn
可以吗?有哪些限制?
在 trait 中使用 假设您不需要出于上述原因之一使用 #[async_trait]
,那么在 trait 中使用常规 async fn
完全没有问题。只需记住,如果您要支持多线程运行时,请使用 #[trait_variant::make]
。
最大的限制是,一个类型必须始终决定它是实现 trait 的 Send 版本还是非 Send 版本。它不能在其泛型之一上有条件地实现 Send 版本。例如,这可能会在 中间件 模式中出现,例如,如果 T: HttpService
,则 RequestLimitingService<T>
就是 HttpService。
#[trait_variant::make]
和 Send
约束?
为什么我需要 在简单的情况下,您可能会发现您的 trait 在多线程执行器中似乎运行良好。但是,有些模式就是无法正常工作。考虑以下情况:
fn spawn_task(service: impl HttpService + 'static) {
tokio::spawn(async move {
let url = Url::from("https://rust-lang.net.cn");
let _body = service.fetch(url).await;
});
}
如果我们的 trait 没有 Send 约束,这将无法编译,并出现错误:“future 不能在线程之间安全地发送”。通过创建具有 Send 约束的 trait 变体,您可以避免将用户引入此陷阱。
请注意,如果您的 trait 不是公共的,您将不会看到警告,因为如果您遇到此问题,您可以稍后自行添加 Send 约束。
我可以混合使用 async fn 和 impl trait 吗?
是的,您可以自由地在 trait 和实现中使用 async fn
和 -> impl Future
拼写。即使一种形式具有 Send 约束,也是如此。3 这使得由 trait_variant
创建的 trait 更易于使用。
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 + '_
?
为什么这些签名不使用 对于 trait 中的 -> impl Trait
,我们提前采用了 2024 年捕获规则。这意味着您今天经常看到的 + '_
在 trait 中是不必要的,因为返回类型已被假定为捕获输入生命周期。在 2024 版中,此规则将适用于所有函数签名。有关更多信息,请参阅链接的 RFC。
-> impl Trait
实现 trait 时会收到“refine”警告?
为什么当我使用 如果您的 impl 签名包含比 trait 本身更详细的信息,您将收到警告
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());
}
多亏了精炼的 trait 实现,它确实可以编译,但编译器会要求您通过在 impl 上使用 #[allow(refining_impl_trait)]
来确认您要精炼 trait 接口的意图。
结论
异步工作组很高兴宣布在 2023 年底完成我们的年度主要目标!感谢所有积极参与设计、实施和稳定讨论的人。还要感谢多年来给予我们宝贵反馈的 Rust 异步用户。我们期待着看到你们构建的作品,并期待在未来几年继续提供改进。
-
请注意,关联类型只能在类型可命名的情况下使用。一旦
impl_trait_in_assoc_type
稳定后,此限制将被取消。↩ -
请注意,在那篇博客文章中,我们最初表示将在发布 traits 中的
async fn
之前解决 Send 边界问题,但我们决定将其从范围中剔除,并改为发布trait-variant
crate。↩ -
这之所以有效,是因为自动 trait 泄露,它允许自动 trait 的知识从签名中未指定它们的项“泄露”出来。↩