1.15 Trait bounds(Trait 约束)的编译与分派1.15.1. 静态分发(static dispatch)编译泛型代码时发生了什么编译器会针对每个T每个类型都将类型或函数复制一部分每个类型都有自己的函数这个过程叫单态化(monomorphization)。详见【Rust自学】10.2.6. 泛型代码的性能。调用dyn Trait上的方法则不同那是动态分发见本文后面部分详见【Rust自学】17.2.3. trait对象执行的是动态派发。当你构建Veci32或HashMapString, bool时编译器会复制它的泛型类型以及所有的实现块。例如Veci32就是把VecT的T替换成i32对Vec做了一个完整的复制所有遇到的T都换成i32。也就是说编译器会把实例的泛型参数使用具体类型替换。需要注意的是编译器其实并不会做完整的复制粘贴他只复制你用的代码。看个例子impl String { pub fn contains(self, p:impl Pattern) - bool { p.is_contained_in(self); } }这个例子针对String类型实现了一个contains方法contains方法的第二个参数p的trait约束是Pattern。p没有实际类型只有trait约束所以p就相当于泛型参数p在实际使用时可能会是不同的类型。针对不同的类型该方法都会复制一遍因为我们需要知道is_contained_in方法的地址以便进行调用。CPU需要知道在哪跳转和继续执行。对于任何给定的p编译器知道那个地址的类型是实现了Patterntrait方法的。不存在一个可给任意类型的通用地址。PS:我知道你在想Python的动态类型Python变量本质上是对象的引用而并非直接存储值。正是因为如此编译器需要为每个类型复制一个方法体每份都有自己的地址来用来跳转。这就是所谓的静态分配(static dispatch)因为对于方法的任何给定副本我们“分派到”的地址都是静态已知的。-静态(static)在编程中通常指编译中已知的事物或可被视为此的1.15.2. 单态化(monomorphization)单态化(monomorphization)指的是从一个泛型类型到多个非泛型类型的过程。Rust的trait就有这个特点。当编译器优化完代码后就好像根本没有泛型。每个实例都是单独优化的具有了所有的已知类型所以上文例子里的is_contained_in方法调用的执行效率就如同trait不存在一样没有任何性能损失。编译器对涉及的类型完全掌握在合适的情况下甚至可以将它们进行inline实现。- “对涉及的类型完全掌握”指的是Rust是静态类型的语言在编译时就能够确定所有变量和函数的类型不需要在运行时进行类型推导。- “将它们进行inline实现”指的是将函数的实现直接展开到调用的地方避免函数调用的开销。#[inline(always)] fn add(a: i32, b: i32) - i32 { a b } fn main() { let x add(2, 3); // 可能被编译器优化为 let x 2 3; }1.15.3. 单态化的代价所有实例都需要单独编译编译时间会因它而增加如果不能优化编译每个单态化的函数会有自己的一段机器码让程序更大指令在泛型方法的不同实例间无法共享CPU的指令缓存效率降低因为它需要持有相同指令的多个不同副本1.15.4. 动态分发(dynamic dispatch)动态分发(dynamic dispatch)使代码可以调用泛型类型上的trait方法而无需知道具体的类型。上面的代码例稍作修改即可实现动态分发impl String { pub fn contains(self, p:impl Pattern) - bool { p.is_contained_in(*self); } }这个例子中实现动态分发需要调用者提供两个信息-Pattern的地址-is_contained_in的地址为什么impl Pattern前要加?- 动态分发依赖于trait 对象而trait对象本质上是一个宽指针fat pointer上一篇文章有讲所以说传入的数据得是一个引用因为Rust不能确定动态分发类型的内存大小所以只能用引用1.15.5. vtable实际上调用者会提供指向一块内存的指针它叫做虚方法表(virtual method table简称vtable)。上例中它持有该类型中所有的trait方法实现的地址其中一个就是is_contained_in这个方法的地址。当代码想要调用提供类型的一个trait方法时就会从vtable查询is_contained_in方法的实现地址并调用。这就允许我们使用相同的函数体而不关心调用者想要使用的类型。每个vtable还包含具体类型的布局和对齐信息(总是需要这些信息配合使用)。1.15.6. 对象安全(Object-Safe)类型实现了一个trait和它的vtable的组合就形成了一个trait object(trait对象)。大部分trait可转为trait object但不是所有。例如Clonetrait就不行(它的clone方法返回self),Extendtrait也不行。这些例子就不是对象安全的(object-safe)。对象安全的具体要求是- trait的所有方法都不能是泛型的也不可以使用self- trait不可以拥有静态方法因为无法知道在那个实例上调用的方法1.15.7.self: Sizedself: Sized意味着self无法用于trait object(因为它是!Sized)。将self: Sized用在某个trait就是要求永远不使用动态分发。我们也可以将self: Sized用在特定方法上这时当trait通过trait object访问的时候该方法就不可用了。当检查trait对象是否安全的时候使用了where self: Sized的方法就会免除1.15.8. 动态分发的优缺点优点缺点编译时间减少编译器无法对特定类型优化提升 CPU 指令缓存效率只能通过 vtable 调用函数直接调用方法的开销增加trait object 上的每次方法调用都需要查 vtable1.15.9. 如何在静态分发和动态分发间选择静态分发动态分发在library中使用静态分发在binary中使用动态分发无法知道用户的需求binary 是最终代码如果使用动态分发用户也只能如此动态分发使代码更整洁省去了泛型参数如果使用静态分发用户可自行选择编译更快以边际性能为代价