2411rust,异步函数

原文

Rust异步工作组很高兴地宣布,在实现在特征中使用异步 fn的目标方面取得了重大进度.将在下周发布稳定的Rust1.75版,会包括特征中支持impl Trait注解和async fn.

稳定化

自从RFC#1522在Rust1.26中稳定下来以来,Rust就允许用户按函数的返回类型(一般叫"RPIT")编写impl Trait.

即该函数返回"某种实现特征的类型".这一般来返回闭包,迭代器和其他复杂或无法显式编写的类型.

cpp 复制代码
//给定一个玩家列表,返回在他们的`名字`上的`一个迭代器`.
fn player_names(
    players: &[Player]
) -> impl Iterator<Item = &String> {
    players
        .iter()
        .map(|p| &p.name)
}

从Rust1.75开始,你可在特征(RPITIT)定义和trait impl中使用返回位置的impl Trait.如,你可用它来编写一个返回迭代器的特征方法:

cpp 复制代码
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的特征.

cpp 复制代码
trait HttpService {
    async fn fetch(&self, url: Url) -> HtmlBody;
//^^^^^^^^变为:
//  fn fetch(&self, url: Url) -> impl Future<Output = HtmlBody>;
}

差距在哪?

公开特征中的->impl特征

仍不建议在公开特征和API中普遍使用->impl Trait,因为用户无法对返回类型加限制.如,无法对容器特征通用的编写此函数:

cpp 复制代码
fn print_in_reverse(container: impl Container) {
    for item in container.items().rev() {
        //错误:^^^对`'impl Iterator<Item=Widget>'`未实现`'DoubleEndedIterator'`特征
        eprintln!("{item}");
    }
}

尽管某些实现可能会返回实现DoubleEndedIterator的迭代器,但在不定义另一个特征时,泛型代码无法利用它.

未来,打算为此添加一个解决方法.当前,->impl Trait最适合内部特征,或当你确信用户不需要额外约束时.否则,应该考虑使用关联类型.

公开特征中的异步函数

因为async fn解糖为->impl Future,因此有同样限制.事实上,如果今天在公开特征中使用空的异步fn,则会看到警告.

警告:不建议在公开特征中使用"async fn",因为无法指定自动特征约束.

异步用户特别感兴趣的是,在返回的未来上的发送约束.因为用户以后无法添加约束,因此错误消息说明你要选择:是否想你的特征与多线程,窃取工作程序一起使用?

好的是,现在有个允许在公开特征中使用异步fn的方法!建议使用trait_variant::make过程宏来让你的用户选择.

该过程宏是由rustlang组织发布的traitvariant包的一部分.在项目中加上cargo add trait-variant.使用:

cpp 复制代码
#[trait_variant::make(HttpService: Send)]
pub trait LocalHttpService {
    async fn fetch(&self, url: Url) -> HtmlBody;
}

这创建两个版本的特征:用LocalHttpService针对单线程执行器,HttpService针对多线程工作窃取执行器.因为后者更常用,因此此例中名字更短.

它有额外的发送约束:

cpp 复制代码
pub trait HttpService: Send {
    fn fetch(
        &self,
        url: Url,
    ) -> impl Future<Output = HtmlBody> + Send;
}

该宏适合异步,因为impl Future很少需要发送以外的额外约束,因此可成功为用户准备好.

动态分发

使用->impl Trait和asyncfn的特征不是对象安全的,即不支持动态分发.准备在未来推出的trait-variant包版本中启用动态分发.

未来如何改进

未来,希望允许用户添加自己的约束到impl Trait返回类型,这样更普遍更有用.它还支持异步fn的更高级用法.语法可能如下:

cpp 复制代码
trait HttpService = LocalHttpService<fetch(): Send> + Send;

因为这些别名不需要特征作者的支持,因此,因此不需要异步特征的发送变量.但是,这些变量仍会方便用户,因此期望大多数包继续提供它们.

常见问题解答

是否可在特征中使用->impl Trait

对私有特征,可自由使用->impl Trait.对公开特征,最好暂时避免使用它们,除非可预见到用户可能需要的所有约束(此时,你可用#[trait_variant::make],与异步一样).

期望取消来会此限制.

是否仍应使用#[async_trait]宏

你可能要继续使用异步特征的原因有几个:

1,想支持低于1.75的Rust版本.

2,你需要动态分发.

如上,希望在未来版本中启用动态分发.

可在特征中使用async fn吗?有哪些限制

假设,你不用#[async_trait],则完全可以在特征中使用普通的异步 fn.如果想支持多线程运行时,记住使用#[trait_variant::make].

最大限制是类型必须总是决定它实现了特征的发送版本还是非发送版本.它不能在其泛型之一上有条件地实现发送版本.

这可在中间件模式中出现,如,如果T:HttpService,则为HttpService的RequestLimitingService<T>.

为什么我需要#[trait_variant::make]和Send约束

简单情况时,发现你的特征似乎与多线程程序配合得很好.但是,有些模式下不管用.考虑以下:

cpp 复制代码
fn spawn_task(service: impl HttpService + 'static) {
    tokio::spawn(async move {
        let url = Url::from("https://rustlang.org");
        let _body = service.fetch(url).await;
    });
}

如果特征上没有Send约束,则无法编译,并显示错误:"不能在线程间安全发送未来".用Send约束创建特征的变量,可避免发送用户掉此陷阱.

注意,如果未公开你的特征,则不会看到警告,因为如果有问题,总是可自行添加发送约束.

见此博客文章.

我可插件使用async fn和impl Trait吗

是的,你可以在特征和实现中的async fn和->implFuture拼写间自由切换.即使一个形式有发送约束,因此.这样更易使用trait_variant创建的特征.

cpp 复制代码
trait HttpService: Send {
    fn fetch(&self, url: Url)
    -> impl Future<Output = HtmlBody> + Send;
}
impl HttpService for MyService {
    async fn fetch(&self, url: Url) -> HtmlBody {
        //只要有`'do_fetch():Send'`就可以了!
        self.client.do_fetch(url).await.into_body()
    }
}

为什么这些签名不使用impl Future+'_

对特征中的->impl Trait,提前用了2024年的抓规则.即今天经常看到的+'_,在特征中是不必要的,因为已假设中类型来抓输入生命期.

在2024版中,此规则针对所有函数签名.

为什么在使用->impl Trait实现特征时收到"细化"警告

如果你的实现签名,包含比特征自身更详细的信息,你会收到警告:

cpp 复制代码
pub trait Foo {
    fn foo(self) -> impl Debug;
}
impl Foo for u32 {
    fn foo(self) -> String {
//^^^^^^警告:`实现方法签名`中的`impl Trait`与`trait`方法签名不匹配
        self.to_string()
    }
}

原因是你可能泄露了更多实现细节.如,如果以下代码编译.

cpp 复制代码
fn main() {
    //实现者允许使用`'显示'`,还是只允许使用`特征`所说的`'调试'`
    println!("{}", 32.foo());
}

因为细化了特征实现,它确实可编译,但编译器会要求你在实现上使用#[allow(refining_impl_trait)],确认你打算细化特征接口.

注意,只能在可以命名类型时,才能使用关联类型.一旦impl_trait_in_assoc_type稳定下来,才取消此限制.

这是因为允许知识从未指定签名它们的项目中"泄漏"的auto trait泄漏.

相关推荐
灿飞屋1 小时前
Rust 编程其实很简单
开发语言·rust
蔡少将6 小时前
用 Tauri 2 给 Mac 应用接 Touch ID,签名公证都做了,还是 -34018
rust
木子欢儿13 小时前
国内 macOS/Linux 安装 rust
linux·运维·服务器·macos·rust
Kapaseker14 小时前
Rust 一行 entry() 干掉五行 if/else
rust
红尘散仙1 天前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
unconditional2 天前
我拆了 sdkm 的回滚设计:每个写操作,都提前写好了翻车预案
rust·开源
数字幽灵2 天前
开源!我用 Rust 手搓了一个 Agent 原生的鸿蒙逆向分析平台
rust
Kapaseker4 天前
Rust 不用写 for 循环?看我怎么写!
rust
奶油话梅糖4 天前
打开项目 0.9 秒、内存只有 IDEA 的 1/12:OpenIDEA 用 Rust 把 IntelliJ IDEA 重造了一遍
java·rust·github·intellij-idea
Vex2une4 天前
Google Picasa 精神续作:Rust 重写的离线本地相册工具‌
rust·图片管理·picasa·马良·本地照片管理·重复图片检测·maliang