返回类型标记 MVP:呼吁测试!

2024 年 9 月 26 日 · Michael Goulet 代表 异步工作组

异步工作组很高兴地宣布,RFC 3654 返回类型标记(RTN)已准备好在 Rust nightly 版本上进行测试。在这篇文章中,我们将简要描述该功能。

背景

Rust 1.75 稳定了 traits 中的 async fn(AFIT)和 traits 中的返回位置 impl Trait(RPITIT)。这些会解糖为匿名泛型关联类型(GAT)。但是,与 GAT 不同,这些类型的用户不能使用 where 子句来进一步限制这些返回类型。这被称为 “send 边界” 问题,因为它通常会影响异步生态系统中 future 的 Send 边界。

一个例子

考虑一个具有 method 方法的 trait Foo,该方法返回 impl Future<Output = ()> 类型。我们想要编写一个函数,该函数调用 method 并将 future 生成到另一个线程上。

fn spawn<T>(f: impl Future<Output = T> + Send + 'static) {}

trait Foo {
    fn method() -> impl Future<Output = ()>; // <-- RPITIT.
}

fn needs_sendable_future<T: Foo>()
where
    // How do we further restrict `T::method()`
    // to be `Send + 'static`?
{
    spawn(T::method());
    //~^ ERROR: `impl Future<Output = ()>` is not `Send`!
}

具体来说,我们可能不想限制 Foo声明,因为在声明中更改它会限制 Foo所有实现。

trait Foo {
    fn method() -> impl Future<Output = ()> + Send + 'static;
    //                                      ~~~~~~~~~~~~~~~~
    //                                      Not what we want.
}

因此,在稳定的 Rust 中,我们在使用 AFIT 或 RPITIT 时无法表达这种限制。相反,如果我们直接使用 GAT,我们今天可以表达这一点。

trait Foo {
    type MethodFuture: Future<Output = ()>;
    fn method() -> Self::MethodFuture;
}

fn needs_sendable_future<T: Foo>()
where
    // We can restrict this to only implementors of `Foo`
    // whose `MethodFuture` is `Send + 'static`, so we can
    // call `spawn` below:
    T::MethodFuture: Send + 'static
{
    spawn(T::method());
}

但是,使用 GAT 意味着 Foo 的实现者必须显式写出返回类型,type MethodFuture = ...,如果我们有一个匿名的、无法命名的 Future 类型,它(尚未)无法工作!

解决方案

RFC 3654 中,我们引入了返回类型标记(RTN)。这将允许我们编写 where 子句边界,这些边界限制了在 traits 中使用 async fn (AFIT) 和 traits 中的返回位置 impl Trait (RPITIT) 的函数和方法的返回类型。扩展上面的示例,RTN 允许我们编写

fn needs_sendable_future<T: Foo>()
where
    T::method(..): Send + 'static // Yay!
{
    spawn(T::method());
    //~^ Works!
}

限制

目前,RTN 仅允许用于具有生命周期泛型(不是 const 或类型泛型)的 trait 关联函数和方法,这些函数和方法使用

  • traits 中的 async fn (AFIT) 或
  • traits 中的返回位置 impl Trait (RPITIT),其中 impl Trait 是最外层的返回类型,即 -> impl Trait,而不是 -> Box<impl Trait>

这些限制在 RFC 3654 中有更详细的描述。

我如何帮助?

我们希望您在最新的 Rust nightly 编译器上测试此功能1

具体来说,我们希望您识别出那些不必要地使用 + Send 或类似边界限制您的 trait 定义的 traits。

// Instead of writing a trait like:

trait Foo {
    fn method() -> impl Future<Output = ()> + Send + 'static;
}

// Write this:

trait Foo {
    async fn method();
}

// And then at the call site, add:

fn use_foo<T: Foo>()
where
    T::method(..): Send + 'static,
{}

类似地,我们希望您识别出当前出于相同原因返回 GAT 的 traits。

// Instead of writing this in the trait and call site:

trait Foo {
    type MethodFuture: Future<Output = ()>;
    fn method() -> Self::MethodFuture;
}

fn use_foo<T: Foo>()
where
    T::MethodFuture: Send + 'static,
{}

// Write this:

trait Foo {
    async fn method();
}

fn use_foo<T: Foo>()
where
    T::method(..): Send + 'static,
{}

但是请注意,我们尚不支持类型位置的 RTN。因此,虽然使用第一个版本,您可以编写

struct Bar<T: Foo> {
    field: T::MethodFuture,
}

但是,使用第二个版本,您还不能编写

struct Bar<T: Foo> {
    field: T::method(..),
}

我们有兴趣了解您会在哪些地方遇到此限制。

我们很高兴 RTN 可以更轻松地在 Send 边界重的异步 Rust 生态系统中使用 traits 中的 async fn (AFIT)。

与往常一样,请查看 RFC 本身,以详细了解我们为什么选择这种设计,特别是 常见问题和理由

  1. 请确保运行 rustup update nightly(或您管理 Rust 版本的方式),因为该功能非常新并且仍然不稳定!