Rust中的特征(trait)可以被看作是其他语言中的接口,它其实是一种约束。通过特征可以直接调用实现了这个特征的数据结构中的方法,根据实现形式不同,这种分发存在静态分发和动态分发两种形式。
在使用静态分发和动态分发的时候,数据对象的类型已经被“抹掉”,编译器只知道正在操作的数据对象实现了指定的特征,并且可以调用特征中约定的方法。
静态分发
程序在运行的时候具体调用哪个数据结构中的函数在编译期就可以确定下来的,都属于静态分发。在Rust中定义静态分发一般是通过泛型和impl Trait
约束来完成的。例如以下两个示例。
|
|
在这个示例中,impl Fn(i32) -> i32
被作为函数参数类型使用,它指明了函数可以接受一个函数作为参数。除了可以用在函数参数上,impl Trait
还可以使用在函数返回值类型上。
|
|
静态分发在用在泛型约束中就不需要使用impl
关键字了,例如把上面的示例改写成泛型约束的形式就是下面这个样子。
|
|
静态分发并不是我们常说的多态,编译器为每一个被泛型参数代替的具体类型都生成了非泛型的函数方法和实现,所以编译器才可能在编译期就确定所要调用的内容。这种处理方法的好处是程序运行速度会很快,但是牺牲的是程序大小。
动态分发
动态分发与静态分发就不一样了,在使用动态分发的时候,编译器是不能在编译期确定被调用的内容的,只能是程序在运行期通过内存寻址才能知道具体调用了什么数据结构的什么方法。动态分发一般使用trait对象来实现,Rust程序在运行时泰国trait抓了中的指针来知晓所需要调用的方法的位置。
Trait对象
Trait对象其实就是到运行时才能够确定对象指代和内容的对象。这种对象的特点就是其大小是无法在编译期测量的,所以为了能够满足Rust编译器的要求,在使用和访问的时候就必须通过指针来进行。
Trait对象的特征首先是对象安全的,这就要求它必须符合以下规则:
- 所有的超类也都必须是对象安全的。
- 超类中不能又
Sized
约束,即不能存在Self: Sized
约束。 - 必须没有任何关联常量。
- 所有关联函数都必须可以从Trait对象中调度分发,或者是显式不可调度分发。
但是在具体实践中,一个trait中的所有方法只要满足了以下两个条件,这个triat就是对象安全的。
- 返回值类型不为
Self
。 - 方法没有任何泛型类型参数。
实际上Trait对象就是另一种类型的不透明值,实现了一组trait,这一组trait由一个对象安全的基础trait加上任意书两类的自动trait组成。
Trait对象在Rust中的书写方式为dyn
关键字后跟一组trait约束,其中要求就是第一个trait必须是基础trait,剩余的trait都必须是自动trait。例如以下trait对象的描述都是合法的。
Trait
dyn Trait
dyn Trait + Send
dyn Trait + Send + Sync
dyn Trait + 'static
或者dyn 'static + Trait
因为trait对象通常都是以引用的形式出现的,所以在作为函数参数使用的时候大多采用&dyn Trait
和Box<dyn Trait>
的形式。
虚表
动态分发在Rust中是通过一张函数指针表来实现的,例如现在有以下定义。
|
|
此时,如果将Cat
实例作为trait对象,那么就会在内存中形成以下结构的布局。
当一个trait对象使用了多个trait约束的时候,trait对象中也就将会出现多个虚表指针分别指向几个不同的虚表。
利用动态分发构建多态程序
多态程序在构建许多需要根据不同的选项、配置采用不同的处理策略时是非常有用的。在这种情况下我们是无法预先得知程序中将要处理什么形式的数据的,采用动态分发就是一个比较理想的解决方案。
这里以一个多协议自动解析的代码为例,首先需要定义一个公共的trait用于表示数据包解析所需要的解析过程。
|
|
之后就需要定义几个实现了这个trait的结构体,和用于表示解析过程中出现的错误的错误枚举类型。
|
|
再接下来就需要定义一个策略函数,用来根据需要生成不同的协议解析器。
|
|
之后就可以在业务代码中这样来使用了。
|
|