这里有一段 Rust 的函数签名:

file

上一篇我们讲了生命周期相关的东西,这次继续。

Box

为了实现面向对象的写法, Rust 里有 trait 语法,就和其他语言类似的 Interface。

面向对象

传统的面向对象语言,以 C# 为例子,会提供抽象类,接口,类可以继承父类,抽象类,实现接口来达到面向对象。但是人们这种方式渐渐的不能够满足编程要求了,这种面向对象的方式太沉重。

举个例子,我抽象了一个动物类 Animal,它有一方法 Run,表示所有继承这个抽象类的都是动物,都能够跑。

(Abstract)Animal - Run

Human -> Animal

随后我发现需要编写一个海豚类,它是动物,需要继承 Animal 抽象类,但是问题是海豚不会跑。无奈之下我又抽象了一个海洋动物类 SeaAnimal,有方法 Swim,表示海洋动物,可以游泳;和一个陆地动物 LandAnimal,有方法 Run;将原本 Animal 里的 Run 方法去除。海豚继承 SeaAnimal,人类继承 LandAnimal。

你能够意识到当功能越多,类越多的时候会修改越来越多的代码,而一旦父级的抽象类受到修改,将影响到每一个继承它的子类。

所以化繁为简,近年来新的语言不再提供抽象类,继承的语法,像 Rust 的 trait,Dart 的 mixin,go 等。取而代之的是实现接口,如果对象需要跑,就实现 Run 接口;如果海豚需要游泳,就实现 Swim 接口;如果人类需要游泳,就实现 Swim 接口。

且可以将接口作为参数,只要实现了这个接口的对象都能够接收,这就是多态

Rust 里的 trait 就用于这里。

多态

但是 Rust 里多态的用法没有其他语言一样透明。

以 TS 为例,接口 IRun,类 Human 实现了 IRun,有一个方法入参类型为 IRun,

function run(input: IRun) {}

let human = new Human();
run(human);

human 可以直接传递给 run,很直观。但是在 Rust 里不能这样写,因为一个 trait,无法在编译时期就知道大小有多大,如果需要在函数里传递 trait,需要使用 impl:

trait Run {}

struct Human;
impl Run for Human {}

fn run(input: impl Run) {}

let human = Human;
run(human);

必须在入参里使用 impl 标注是一个 trait,其实这样也能接收对吧。但是如果你要在 struct 里的一个字段类型是 trait,就不能这么写。

struct Human {
	va: impl Run // compile error
}

因为编译器无法推断出大小。你得用泛型静态分配或者是使用 Box 动态分配。

动态分配

Box 可以将一个对象放在堆上,并使用一个指针指向它。如果使用 dyn 关键字,后跟着一个 trait,那么这个 Box 就能够持有任何实现了这个 trait 的对象。

struct Human {
	va: Box<dyn Run>
}

let human = Human {
	va: Box::new(RunFast)
}

就能够在 struct 持有一个 trait 的字段,以实现多态。Box 的方式也能够在函数入参里使用。但 Box 的动态分配并不能动态分配所有 trait,在下面会讲到。

Async

Rust 的 asyncawait 语法其实是 Future 的语法糖。图片里的函数是 async_trait 生成的 Future 函数,你能看到它的返回值其实是 Pin<Box<Future>> 而不是 Future,这是因为 Rust 并没有对异步做到很舒服的支持。

在 1.75 版本以前 Rust 还不支持在 trait 中定义异步方法。

// 1.75 版本以前无效
trait Run {
	async fn run();
}

但是我们仍然有这个需求,所以需要使用 async_trait crate 来支持 trait 里的 async 方法,具体看 async_trait 包。

async_trait 所做的就是将返回值变成 Pin<Box<dyn Future<...>>>。Box 的作用我们前面讲了,配合 dyn 关键字可以实现多态。而 Pin 是什么。

Pin 包装的对象不能移动在内存中的位置。这是因为异步方法可能会被移动带其他线程中,而改变位置,此时如果有一个指向原来位置的指针,会因为对象被 runtime 移动了而变成一个指向错误位置的指针。所以需要使用 Pin 使它不能被移动。

现在的版本都是 1.85 了,早就支持 trait 里的 async 语法了,为什么还要使用 async_trait 呢?这是因为定义了异步方法的 trait 无法在 Box 里动态分配。

trait Run {
	async fn run(&self);
}

struct Human {
	va: Box<dyn Run>
}

编译器会抱怨

the trait `Run` cannot be made into an object
consider moving `run` to another trait

Run 无法转变成一个对象,很迷惑的错误信息。让我们来脱糖一下这个 async 方法,会变成:

fn run(&self) -> impl Future<Output = ()>;

脱糖后的返回值是一个 trait,编译器无法在编译阶段就知道其大小,无法编译(你 TM!!!)。所以无法被 Box 持有。

async_trait 做的的就是将返回值的 Future 包装进 Box 里,让返回值变成一个在编译阶段就可知的指向堆的指针,当然还有 Pin。

这就是为什么图片里的返回值是 Pin<Box<dyn Future<Output = ...>>>

Marker

在返回值的 Box 里的 Future 还跟着一个 Send 约束,这个 Send 是一个 marker,被 Send 标记的对象表示可以被移动到其他线程。因为异步对象可能会被移动到其他线程,如果 Future 没有 Send 标记的话是不能移动到其他线程的,所以需要 Send 标记。

Rust 里所有 primitive 类型 都是 Send 的,如果一个 struct 里所有字段都是 Send 的,那么这个 struct 也自动变成 Send 的。

随后还跟着一个 'async_traic 生命周期注解,在上一篇我们见过生命周期,这个'async_traic 标记在 Box 里,表示 Box 持有的借用生命周期不能小于 'async_traic,或者持有所有权。

到此,就构成了图片里的函数签名。