如何在trait中定义异步方法?

23

我有一个特点,我正在使用它来抽象化tokio::net::TcpStreamtokio::net::UnixStream

/// Interface for TcpStream and UnixStream.
trait TryRead {
  // overlapping the name makes it hard to work with
  fn do_try_read(&self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
}

impl TryRead for TcpStream {
  fn do_try_read(&self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
      self.try_read(buf)
  }
}

问题在于我想在两种方法中抽象出pub async fn readable(&self) -> io::Result<()>,但是异步方法不能在特征中实现。我该如何处理?

4个回答

49

在稳定版的Rust中,async fn不能在trait中使用。目前正在进行的工作将来会使这成为可能,但目前最简单的解决方案是使用async-trait crate:

#[async_trait]
trait Readable {
    async fn readable(&self) -> io::Result<()>;
}

#[async_trait]
impl Readable for Reader {
    async fn readable(&self) -> io::Result<()> {
        do_stuff().await
    }
}

为了避免将Send绑定放置在async特质方法上,您可以在特质和实现块上都调用异步特质宏,如#[async_trait(?Send)]

#![feature(async_fn_in_trait)]

在夜间版中,现在可以使用async_fn_in_trait功能编写异步特质方法:

#![feature(async_fn_in_trait)]

trait Readable {
    async fn readable(&self) -> io::Result<()>;
}


impl Readable for Reader {
    async fn readable(&self) -> io::Result<()> {
        do_stuff().await
    }
}

然而,目前的实现存在局限性,不允许在返回的future上指定SendSync边界。详情请参见公告帖子

关联类型

另一种方法是使用关联类型:

trait Readable {
    type Output: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output;
}

实现此特质时,您可以使用任何实现了 Future 的类型,例如标准库中的 Ready

use std::future;

impl Readable for Reader {
    type Output = future::Ready<io::Result<()>>;
    
    fn readable(&self) -> Self::Output {
        future::ready(Ok(()))
    }
}

async函数返回一个不透明的impl Future,因此如果您需要在函数内部调用它,则不能有具体的Output类型。相反,您可以返回一个动态类型的Future

impl Readable for Reader {
    // or use the handy type alias from the futures crate:
    // futures::BoxFuture<'static, io::Result<()>>
    type Output = Pin<Box<dyn Future<Output = io::Result<()>>>>;
    
    fn readable(&self) -> Self::Output {
        Box::pin(async {
            do_stuff().await
        })
    }
}

请注意,使用这些特质方法将导致每次函数调用都产生堆分配和动态分派,就像使用async-trait crate一样。 对于绝大多数应用程序来说,这不是一个重要的成本,但需要考虑。

捕获引用

可能出现的一个问题是,关联类型Output没有生命周期,因此无法捕获任何引用:

struct Reader(String);

impl Readable for Reader {
    type Output = Pin<Box<dyn Future<Output = io::Result<()>>>>;
    
    fn readable(&self) -> Self::Output {
        Box::pin(async move {
            println!("{}", self.0);
            Ok(())
        })
    }
}

error: lifetime may not live long enough
  --> src/lib.rs:17:9
   |
16 |       fn readable(&self) -> Self::Output {
   |                   - let's call the lifetime of this reference `'1`
17 | /         Box::pin(async move {
18 | |             println!("{}", self.0);
19 | |             Ok(())
20 | |         })
   | |__________^ returning this value requires that `'1` must outlive `'static`

在稳定的 Rust 中,关联类型不能具有生命周期,因此您需要将输出限制为一个捕获了 self 的 boxed future,以使这种情况成为可能:

trait Readable {
    // note the anonymous lifetime ('_) that refers to &self
    fn readable(&self) -> Pin<Box<dyn Future<Output = io::Result<()>> + '_>>;
}

impl Readable for Reader {
    fn readable(&self) -> Pin<Box<dyn Future<Output = io::Result<()>> + '_>> {
        Box::pin(async move {
            println!("{}", self.0);
            Ok(())
        })
    }
}

实际上,这正是async-trait包在内部执行的操作。

不稳定特性

如果您使用的是夜间版本,则情况会更好。您可以启用type_alias_impl_trait功能并使用常规的async/await语法而无需进行包装:

#![feature(type_alias_impl_trait)]

trait Readable {
    type Output: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output;
}


impl Readable for Reader {
    type Output = impl Future<Output = io::Result<()>>;
    
    fn readable(&self) -> Self::Output {
        async { ... }
    }
}

使用上述代码仍然存在借用问题。但是,由于最近稳定的泛型关联类型功能,您可以将Output泛型化为生命周期并捕获self

trait Readable {
    type Output<'a>: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output<'_>;
}

而且,前面的例子编译时不会发生装箱操作!

struct Reader(String);

impl Readable for Reader {
    type Output<'a> = impl Future<Output = io::Result<()>> + 'a;
    
    fn readable(&self) -> Self::Output<'_> {
        Box::pin(async move {
            println!("{}", self.0); // we can capture self!
            Ok(())
        })
    }
}

3
值得注意的是,async-trait 可以通过在 trait 和实现上使用 #[async_trait(?Send)] 来避免 Send 约束。 - apetranzilla
@ruipacheco 我在我的回答中提到了这一点:“在你无法静态类型化结果的情况下,你可以返回一个拥有动态类型 Future”。 - Ibraheem Ahmed
1
我已经重构了我的代码以使用async_trait,但仍然有相同的问题。 - ruipacheco
在我的调用代码中,是的。我应该在新特性内部完成吗? - ruipacheco
1
rust-analyzer 给了我错误的输出,问题已解决。谢谢! - ruipacheco
显示剩余2条评论

4

目前,无法在特质中使用async方法。虽然这个功能将被稳定下来(并且可能需要相当长的时间),但我所知道的唯一解决方案是使用async_traitcrate。

use async_trait::async_trait;

#[async_trait]
trait Readable {
    fn async readable(&self) -> io::Result<()>;
}
< p > async_trait宏基本上只是将您的函数转换为以下代码:

trait Readable {
    fn readable<'a>(&self) -> Pin<Box<dyn 'a + Send + Future<Output = io::Result<()>>>
}

这种方法的缺点是需要一个特质对象,这会增加额外的成本。

2
async-trait 将您的函数转换为带有 Send 约束的 Pin<Box<dyn Future<...>>> - Ibraheem Ahmed
可以有人更新一下这个例子以备将来参考吗? - ruipacheco
好的,完成了,谢谢你指出来。我很确定我记得代码是如何扩展的。 - Gymore

1

所以这是“我读到了一些可能有帮助的东西,但我从未尝试过。”?你能试试[答案]吗? - Yunnosch
抱歉,我注意到文章中已经有一个示例了,所以我没有创建自己的示例。我在这里提供了自己的示例,感谢您的提醒。 - user9032741
你所添加的只是一个链接,这并不被认为是对StackOverflow答案的贡献。 - Yunnosch

0
只是一个快速的更新:async_fn_in_trait pull request已经合并,应该在稳定的Rust版本1.75中,预计于2023年12月28日发布。请查看pull request以获取有关此PR启用的完整详细信息。简而言之,您将能够执行以下操作(代码示例取自PR):
trait Bar {
    async fn bar(self);
}

impl Bar for () {
    async fn bar(self) {}
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接