前言
受限于Rust类型系统,在Rust 1.75之前,trait接口无法定义异步函数,并且无法使用impl Struct作为返回值。但在Rust 1.75稳定版,这些特性均得到支持。
简述
在Rust 1.26上,Rust稳定了一个特性:支持开发者使用impl Trait
作为函数的返回值,以下是一个例子:
1
2
3
4
5
6
7
8
/// 给定一个匿名生命周期的players,返回其对应的同等生命周期的迭代器。
fn player_names(
players: &[Player]
) -> impl Iterator<Item = &String> {
players
.iter()
.map(|p| &p.name)
}
但是类似impl Iterator<Item = &String>
无法出现在traits内部接口函数中。从Rust 1.75版本开始,这类写法得到支持,成为了新语法糖。
trait Container {
fn items(&self) -> impl Iterator<Item = Widget>;
}
impl Container for MyContainer {
fn items(&self) -> impl Iterator<Item = Widget> {
self.items.iter().cloned()
}
}
另外对于async fn
,事实上还是会被解析成fn。考虑一个trait:
trait HttpService {
async fn fetch(&self, url: Url) -> HtmlBody;
}
事实上会被解析为:
fn fetch(&'a self, url: Url) -> impl Future<Output = HtmlBody>;
笔者十分感叹,在刚开始写Rust的时候,还是版本1.49版本,当时连GAT都没有,上述写法impl Future还不支持。只能使用async-trait
完成,但是async-trait
使用宏,解构后实际上还是使用了Box数据结构,会导致堆分配,造成性能问题。
最佳使用实践
这两个特性尽量不在public公用traits中出现。
impl Trait
在公用trait中出现
考虑一个trait:
trait Test {
fn print_in_reverse(container: impl Container);
}
container
类型为impl Container
,在实现这个trait的struct内重写这个函数,container无法增加任何约束,比如:
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}");
}
}
rev()
需要类实现DoubleEndedIterator trait,若业务层需要这个rev,则会导致写起来十分困难,目前Rust 1.75版本不支持实现这个接口函数的时候给这个参数施加更多的约束。以下是报错信息:
Rust官方后期通过其他手段来解决这个trait问题。
所以Rust社区鼓励在私有trait中使用impl Trait,但是不鼓励在公有trait中使用,因为私有trait只有开发者内部使用,其知道需要增加哪些约束,在写这个Trait的时候就已经加好了。但是公有trait,不同开发者使用这个trait时可能根据业务需求增加约束,但是目前Rust又不支持。
async fn
在公用trait中出现
由于async fn
就是一个语法糖,语法糖必然容易出现不灵活的地方。
考虑以下一个trait函数定义:
async fn fetch(&self, url: Url) -> HtmlBody;
以上定义了一个异步函数,传入一个url,返回一个HtmlBody。实际上,在Rust编译器解析后,会解语法糖为:
fn fetch(&self, url: Url) -> impl Future<Output = HtmlBody>;
解构后,fetch函数返回的Future
事实上是没有实现Send
的,这会直接限制这个Future在线程中传递。即async fn
的返回值无法设置Send
约束。
异步分为两种情况:一种是多线程任务窃取机制运行,另一种是epoll/io-uring机制下单线程异步运行。多线程环境下需要满足变量满足Send
trait,表明变量可在线程之间安全传递。单线程则不需要满足Send
。
Send
无法应用于返回值,直接导致了某些异步写法无法使用,因此,Rust社区提出了trait_variant
用于快速将一个async fn
trait转换为两个trait,一个满足Send、一个不满足Send,用于支持单线程和多线程异步环境。
使用了二者特性的trait不支持动态分发
使用了-> impl Trait
和async fn
并不是对象安全的,而动态分发的要求是trait是对象安全的。导致这类trait无法动态分发,Rust社区承诺将在后期通过trait_variant
解决。
加餐,对象安全是什么:
当一个 trait 不是 object-safe 的时候,意味着它无法在运行时进行动态分发。这是因为 trait objects 需要在编译时知道有关 trait 和方法的具体信息,以便正确地调用方法。如果一个 trait 不满足 object-safe 的条件,编译器无法保证在运行时能够正确地处理相关的类型和方法调用,因此无法用于动态分发。
关联类型必须是 Sized:
如果一个 trait 中的关联类型不是 Sized,那么编译器无法确定这些类型的大小,在编译时无法生成正确的调用代码。 例如,假设一个 trait 的方法返回一个关联类型,但这个关联类型不是 Sized,那么编译器无法预先分配内存空间来存储返回值,也无法确定如何正确地传递和使用这个返回值。
方法参数和返回值的确定性:
Object-safe 的 trait 要求方法不能有泛型参数,并且不能返回 Self 类型。这是因为在动态分发时,编译器需要知道每个方法的确切参数类型和返回类型,以便正确地调用这些方法。
如果一个 trait 的方法有泛型参数,或者返回 Self 类型,这就会导致不确定性,编译器无法在编译时确定这些类型,因此无法生成正确的调用代码。
object-safe 的 trait 需要满足一系列的限制条件,以便编译器在编译时能够准确地生成 trait objects 所需的信息。如果一个 trait 不满足这些限制条件,编译器无法在运行时保证能够正确地处理相关的类型和方法调用,因此无法用于动态分发。