动态分派和静态分派

Rust可以同时支持“静态分派(static dispatch)”和“动态分派(dynamic dispatch)”。

静态分派

指的是具体调用哪个函数,在编译阶段就确定下来了。
Rust中的“静态分派”靠泛型来完成。对于不同的泛型类型参数,编译器会生成不同版本的函数,在编译阶段就确定好了应该调用哪个函数。

动态分派

指的是具体调用哪个函数,在执行阶段才能确定。Rust中的“动态分派”靠 Trait Object 来完成。Trait Object 本质上是指针,它可以指向不同的类型,指向的具体类型不同,调用的方法也就不同。

示例

trait Bird,另外有两个类型,都实现了这个trait

  1. trait Bird {
  2. fn fly(&self);
  3. }
  4. struct Duck;
  5. struct Swan;
  6. impl Bird for Duck {
  7. fn fly(&self) { println!("duck duck"); }
  8. }
  9. impl Bird for Swan {
  10. fn fly(&self) { println!("swan swan");}
  11. }

trait是一种DST(Dynamic Sized Type)类型,它的大小在编译阶段是不固定的。这意味着这样的代码是无法编译通过的:

  1. fn test(arg: Bird) {}
  2. fn test() -> Bird {}

因为 Bird 是一个 trait,而不是具体类型,它的 size 无法在编译阶段确定,所以编译器是不允许直接使用 trait作为参数类型和返回类型的。这也是 trait跟许多语言中的 “interface” 的一个区别。

泛型的实现

利用泛型

  1. fn test(arg: impl Bird) { arg.fly(); }
  2. }

通过指针实现“多态”
虽然 traitDST类型,但是指向 trait的指针不是 DST。如果我们把 trait隐藏到指针的后面,那它就是一个 trait object,而它是可以作为参数和返回类型的。

根据不同需求,可以用不同指针类型

  • Box
  • &
  • &mut
  1. fn test(arg: Box<Bird>) {
  2. arg.fly();
  3. }

这种方式,test函数的参数既可以是 Box<Duck> 类型,也可以是 Box<Swan> 类型,一样实现了“多态”

内部原理

知乎版

同理,Bird只是一个trait的名字,符合这个trait的具体类型可能有多种多样,这些类型并不具备同样的大小,因此Bird也是一个DST类型。指向Bird的指针理所当然也应该是一个“胖指针”,它的名字就叫Trait Object。它的内部表示如下所示:

  1. pub struct TraitObject {
  2. pub data: *mut (), // 指向具体的对象
  3. pub vtable: *mut (), // 指向虚函数表,虚函数表中存储为这个类型实现的trait信息
  4. }

未命名文件(1).png

Rust的动态分派,和C++的动态分派,内存布局有所不同。在C++里,如果一个类型里面有虚函数,那么每一个这种类型的变量(实例),内部都包含一个指向虚函数表的地址。而在 Rust 里面,对象本身不包含指向虚函数表的指针,这个指针是存在于 trait object 指针里面。如果一个类型实现了多个 trait,那么不同的 trait object 指向的虚函数表也不一样。

视频版

Rust内存Layout
image.png

The “vtable” is generated once at compile-time and shared by all objects of the same type

vtable在编译时只生成一次,并由所有相同类型的对象共享(例如,为Struct A 实现了Writer trait,那么所有A类型的实例(对象)都用到同一个vtable)

Such virtual table exist for each type that implements the trait, but each instance of the same type share the same virtual table.

每个实现该特征的类型都存在这样的虚表,但是相同类型的每个实例共享相同的虚表。

The “vtable” contains pointers to the machine code of the functions that must be present for a type to be a “Writer”

虚函数表包含指向函数机器码的指针,这些机器码是类型为“Writer”时必须存在的。

at the point where this conversion happens, Rust knows the referent’s true type

当这个转换发生时,Rust知道引用对象的真实类型

总结:这里以Struct这种自定义类型代替上面说到的“类型”两字。
为不同的Struct实现同一个trait时,会为每一个Struct生成一个vtable,当生成trait object时,会有一个data指针指向这个实例,一个vtable指针指向这个trait下实例所属Struct的vtable

Object safe

为trait加Self:Sized约束时

Self关键字代表的类型是实现该trait的具体类型
因为traitunSized的,所以加上**Self: Sized**约束后,不能创建trait object
如下所示:

  1. trait Foo where Self: Sized {
  2. fn foo(&self);
  3. }
  4. impl Foo for i32 {
  5. fn foo(&self) { println!("{}", self); }
  6. }
  7. fn main() {
  8. let x = 1_i32;
  9. x.foo();
  10. // 不允许创建trait object
  11. // 报错:error: the trait `Foo` cannot be made into an object [E0038]
  12. //let p = &x as &Foo;
  13. //p.foo();
  14. }

因此,如果我们不希望一个 trait通过trait object的方式使用,可以为它加上**Self: Sized**约束。

为trait中的方法加Self:Sized约束

可以为方法单独加**Self:Sized**约束,这样就不能通过trait object的方式调用这个方法了
如下:

  1. trait Foo {
  2. fn foo1(&self);
  3. fn foo2(&self) where Self: Sized;
  4. }
  5. impl Foo for i32 {
  6. fn foo1(&self) { println!("foo1 {}", self); }
  7. fn foo2(&self) { println!("foo2 {}", self); }
  8. }
  9. fn main() {
  10. let x = 1_i32;
  11. x.foo2();
  12. let p = &x as &Foo;
  13. // 无法调用
  14. p.foo2();
  15. }

返回类型是Self

Self类型代表的是,impl这个 trait的当前类型

如果trait中有方法返回Self类型,那么它也不能成为trait object
如下:

  1. pub trait Clone {
  2. fn clone(&self) -> Self;
  3. fn clone_from(&mut self, source: &Self) { ... }
  4. }
  5. fn main() {
  6. let s = String::new();
  7. let p : &Clone = &s as &Clone();
  8. }
  9. 报错:
  10. error: the trait `std::clone::Clone` cannot be made into an object

Rust规定,如果函数中,除了self这个参数之外,如果还在其它参数或者返回值中用到了Self类型,那这个函数就不是object safe的。

如果仍然想要使用trait object,则需要为使用Self类型的方法加上**Self: Sized**约束
如下

  1. fn new() -> Self where Self: Sized;

这样,编译器为这个trait生成vtable时,会忽略这个方法

trait中存在静态方法

编译器没有办法把静态方法加入到虚函数表中。

如果一个 trait 中存在静态方法,而又希望通过trait object 来调用其它的方法,那么我们需要在这个静态方法后面加上 Self: Sized 约束,将它从虚函数表中剔除出去。

trait中存在使用泛型的方法

Rust选择的解决方案是,禁止使用trait object来调用泛型函数,泛型函数是从虚函数表中剔除掉了的

参考
trait Object