Zach的博客

Trait Object

Trait Object

静态分派和动态分派

Rust支持静态分派(static dispatch)和动态分派(dynamic dispatch)。

静态分配是在编译阶段就确定好了应该调用哪个函数,它是通过泛型实现的多态。一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait Birt {
fn fly(&self);
}
struct Duck;
struct Swan;

impl Bird for Duck {
fn fly(&self) { println!("Duck fly");}
}

impl Bird for Swan {
fn fly(&self) { println!("Swan fly");}
}

fn test<T: Bird>(arg: T) {
arg.fly()
}

上述例子就是利用泛型实现了多态,在编译阶段,编译器会根据实际调用的参数不同,直接生成不同的函数版本:

1
2
3
4
5
6
7
8
// 伪代码
fn test_Duck(arg: Duck) {
arg.fly();
}

fn test_Swan(arg: Swan) {
arg.fly();
}

动态分派就是调用哪个函数必须在运行时才能决定。Rust中动态分派是靠Traint Object实现的,一个Trait Object就是执行Trait的指针,如果Bird是一个Trait,那么&Bird,Box都是Trait Object。

一个指向Trait的指针是一个胖指针,它不仅包含所指向的对象的元素,也包含一个虚函数表,用以在运行时决定这个Trait Object对应的函数实现调用。Rust中一个Trait Object的内部表示如下:

1
2
3
4
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}

Object Safe

Trait Object的构造受到很多约束,在约束不能满足的时候就会产生编译错误。

  • trait有Self:Sized约束时,不能构造Trait Object。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Foo where Self: Sized {
fn foo(&self);
}

impl Foo for i32 {
fn foo(&self) {..}
}

fn main() {
let x = 1_i32;
x.foo();
let p = &x as &Foo; // error
p.foo();
}

上述代码会产生编译错误。

如果Trait没有加上Self: Sized约束,而Trait中某一个某一个方法加上了Self: Sized约束,那么这个方法不会出现在Trait Object中。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
trait Foo {
fn foo1(&self);
fn foo2(&self) where Self:Sized;
}
impl Foo for i32 {
fn foo1(&self) {
println!("Foo1");
}

fn foo2(&self) {
println!("Foo2");
}
}

fn main() {
let x = 1i32;
x.foo2();

let p = &x as &Foo;
p.foo2(); // error
}
  • 当函数中有Self类型作为参数或者返回类型时(不包括self这个参数),不能构造Trait Object

有这样的一个例子:

1
2
3
4
5
6
7
pub trait Clone {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {...}
}
// 伪代码
let p : &Clone = xxx;
let o = p.clone();

由于p时一个trait object,编译器只知道它是一个胖指针,包含了具体对象的指针以及指向虚函数表的指针。p指向具体的对象,它的类型是什么已经不知道了,编译器知道的只是这个对象实现了Clone这个Trait,而clone方法却要求返回一个与p指向的对象一样的类型的值,这个是无法完成的任务,所以这个Clone不是object safe的。

那么如果我们需要让一个Trait满足Object safe同时又要能够构造Trait Object该怎么办呢,还记得上面提到的Self: Sized约束吧,利用它就可以了。

1
2
3
4
trait Double {
fn new() -> Self where Self: Sized;
fn double(*mut self);
}

这样之后我们就可以狗仔一个trait object了,当然这个trait object的虚函数表里不存在new这个函数。

  • 当函数有泛型参数时,不能构造trait object

假如有这样的trait

1
2
3
trait SomeTrait {
fn generic_fn<A>(&self, value: A);
}

如果我们使用trait object来调用这个函数:

1
2
3
4
fn func(x: &SomeTrait) {
x.generic_fn("foo");
x.generic_fn(1u8);
}

这样会有一个问题,x是一个trait object,调用它的方法时是通过vtable虚函数表来进行查找并调用的,现在要调用的函数成了泛型函数,而泛型是在编译阶段自动展开的,generic_fn函数实际上有许多不同的版本,而把这些版本都塞进去是很困难的,Rust直接禁止使用trait object来调用泛型函数。

  • 如果有静态方法,那么这个Trait不适Object safe的。

要构造这样的Trait Object,解决方法依然是利用Self: Sized约束。