Rust trait 对象与动态分发
静态分发与动态分发
回顾第十章'泛型代码的性能'中讨论的内容,当对泛型使用 trait bound 时,编译器会进行单态化处理:为每一个被泛型类型参数代替的具体类型生成非泛型的函数和方法实现。单态化所产生的代码进行静态分发(static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。
这与动态分发(dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。
当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择地内联方法代码,这会相应地禁用一些优化。尽管在编写示例支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
Trait 对象需要类型安全
只有对象安全(object-safe)的 trait 才能实现为特征对象。这里有一些复杂的规则来实现 trait 的对象安全,但在实践中,只有两个相关的规则。如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:
- 返回值不是 Self
- 没有泛型类型的参数
Self 关键字是我们在 trait 与方法上的实现的别称,trait 对象必须是对象安全的,因为一旦使用 trait 对象,Rust 将不再知晓该实现的返回类型。如果一个 trait 的方法返回了一个 Self 类型,但是该 trait 对象忘记了 Self 的确切类型,那么该方法将不能使用原本的类型。当 trait 使用具体类型填充的泛型类型时也一样:具体类型成为实现 trait 的对象的一部分,当使用 trait 对象却忘了类型是什么时,无法知道应该用什么类型来填充泛型类型。
Clone trait 示例
一个非对象安全的 trait 例子是标准库中的 Clone trait。Clone trait 中的 clone 方法的声明如下:
pub trait Clone {
fn clone(&self) -> Self;
}
String 类型实现了 Clone trait,当我们在 String 的实例对象上调用 clone 方法时,我们会得到一个 String 类型实例对象。相似地,如果我们调用 Vec 实例对象上的 clone 方法,我们会得到一个 Vec 类型的实例对象。clone 方法的标签需要知道哪个类型是 Self 类型,因为 Self 是它的返回类型。
当我们尝试编译一些违反 trait 对象的对象安全规则的代码时,我们会收到编译器的提示。例如,我们想实现 Screen 结构体来保存一个实现了 Clone trait 而不是 Draw trait 的类型,如下所示:
pub struct Screen {
pub components: Vec<Box<dyn Clone>>,
}
我们将会收到如下错误:
Compiling gui v0.1.0 (file:///projects/gui)
error[E0038]: the trait `Clone` cannot be made into an object
--> src/lib.rs:2:29
|
2 | pub components: Vec<Box<dyn Clone>>,
| ^^^^^^ `Clone` cannot be made into an object
|
= note: the trait cannot be made into an object because it requires `Self: Sized`
= note: for a trait to be 'object safe' it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit https://doc.rust-lang.org/nightly/std/marker/trait.ObjectSafe.html
这个错误意味着我们不能将此 trait 用于 trait 对象。

