Rust中同时支持动态分派和静态分派。
静态分派
所谓静态分派,是指编译器就知道调用哪一个函数来实现多态。举个例子来说,我们有一个Foo Trait,并为u8和String实现了这个Trait:
1 | trait Foo { |
如果我们定义如下的泛型函数
1 | fn do_something<T: Foo>(x: T) { |
并这样调用这一个泛型函数
1 | fn main() { |
由于在编译阶段编译器已经知道了x和y的类型,那么编译器会在调用泛型函数的时候展开函数,即编译器会生成类似下面的代码:
1 | fn do_something_u8(x: u8) { |
这样一来,泛型函数的调用就变成了特定类型展开结果的内联调用,这样的优点是显而易见的:内联函数易于优化,而且快!当然,内联函数过多,代码也会变成臃肿,同时”优化“也并不见得是真的优化。
动态分派
由于静态分派有瑕疵,也就有了动态分派。动态分派是通过Trait Object
实现的,Trait Object
指的是类型直到运行时才能确定的对象,如&Foo,Box\
一个Trait Object
其实是一个胖指针,它的内存结构为:
1 |
|
可以看到Trait Object
是通过虚函数表来实现动态分派的,但是Rust的虚函数表和C++的虚函数表不同的是,在C++中,每一个对象实例中都存在一个vtable指针,而在Rust中,对象实例是没有vtable指针的,vtable阵阵存在于Trait Object
中。
一个vtable实际上就是包裹了函数指针的结构,还是以Foo这个Trait为例子,一个可能的实现如下:
1 | struct FooVtable { |
这样之后,编译器就可以把let p = &x as &Foo
做如下的转换:
1 | // let p = &x as &Foo |
调用Trait Object
对应的方法也就转换成了:
1 | // p.method() |
Object Safe
并不是所有的Trait都可以有相应的Trait Object
的,一个Trait如果想要构造出一个Trait Object
,那么这个Trait必须时Object Safe的,那什么是Object Safe呢?它必须满足以下两个条件:
trait没有
Self: Sized
约束trait中所有的方法都是object safe的
trait中所有的方法都是Object Safe的是指:
不能有任何的类型参数,即方法不能是泛型的
方式中没有Self参数
一个Trait 只是描述了一个类型的公共行为,它并不知道这个类型的具体实现的大小,也就是说实现这些公共行为的具体类型大小各异,并不受拘束,那么如果我们给一个Trait加上
Self: Sized
约束,这就是告诉我们,这个Trait所描述的具体类型大小在编译器就知道了,即它的大小是一个常量,而这与Trait Object
是相违背的。所以如果我们像下面一样定义一个Trait,我们是没有办法构造出一个Trait Object
的。1
2
3
4
5
6
7trait Foo where Self: Sized {
fn foo(&self);
}
...
// let p = &x as &Foo; // error有时候我们只是想让某些方法不出现在vtable中,而不是构造不出
Trait Object
,这个时候只要给对应的方法加上Self: Sized
约束即可。即:1
2
3
4
5
6
7
8
9
10trait Foo {
fn foo(&self);
fn not_in_vtable(&self) where Self: Sized;
}
...
let p = &x as &Foo;
p.foo(); // corrent
// p.not_in_vtable(); // error一个trait方法如果有一个Self参数会发生什么呢?拿Clone举个例子,
1
2
3
4
5
6
7
8
9
10pub trait Clone {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) { ... }
}
...
let p = &x as &Clone;
let y = p.clone();首先由x得到了一个
Trait Object
,这个时候编译器已经不知道x原来的类型是什么了,只是只要由p可以调用一些列Clone所定义的方法,而接下来又要调用clone这个函数来复制x的一个实例,这就让编译器犯难了,即然它都不知道原先的类型是啥,又何谈复制?所以Rust就规定这种情况下,Trait不是Object Safe的,也就不能构造出Trait Object
。如果一个trait方法没有self参数,即为一个静态方法,那么很显然静态方法是没有办法放进vtable中的,所以静态方法也不是object safe的
如果trait方法中有类型参数,比如:
1
2
3trait Foo {
fn foo<T>(&self, t: T);
}由于泛型参数是在编译期自动展开的,如果要讲泛型函数放到vtable中,就必须把所有的foo版本都赛到虚函数表中,这个就太为难Rust编译器了,所以Rust规定,如果有类型参数,那么方法也不是Object Safe的。