调度是选择一个具体函数(方法)进行调用的机制。在Rust中有两种方法:静态调度和动态调度。
问题的背景:
在面向对象编程语言中,动态调用方法通常使用vtable(虚拟表)。在Rust中,类似的实现用于trait对象,即对实现特定trait的类型对象的引用。当使用泛型和trait bounds时,会出现静态调度。
问题:
通常在灵活性(能够通过一个接口处理不同类型的对象)和性能(静态调度允许内联方法)之间做出选择。不正确的选择会导致泛型过于复杂,或者性能损失。
解决方案:
静态调度是通过generic参数实现的;在这种情况下,编译器为每种类型生成单独的代码。动态调度是指如果函数接受类型为&dyn Trait或Box<dyn Trait>的参数,那么Rust会在调用trait的方法时,查看vtable的地址,这与经典的面向对象编程语言相同。
代码示例:
trait Shape { fn area(&self) -> f64; } impl Shape for Circle { fn area(&self) -> f64 { 3.1415 * self.radius * self.radius } } fn print_area(shape: &dyn Shape) { // 动态调度 println!("area = {}", shape.area()); } // 或者静态的: fn print_area_static<S: Shape>(shape: &S) { println!("area = {}", shape.area()); }
关键特点:
dyn Trait使用vtable(动态调度)可以做Box<dyn Sized>吗?
不可以。dyn Trait的定义是—unsized,总是需要使用Box、Arc或引用,但不能是“Box<dyn Sized>”—这没有意义。Sized trait不具备trait对象。
对于具有泛型方法的trait,dyn Trait被允许吗?
不允许。不能制作具有泛型方法的object-safe trait(容易混淆!),复合类型不是object-safe:
trait MyTrait { fn foo<T>(&self, x: T); } let x: &dyn MyTrait = ... // 编译错误!
可以为带有Self值的签名的trait制作dyn Trait吗?
不可以,如果方法返回Self(许多人不理解这个细微之处:object safety要求签名中不包含Self;self只能在参数中,但不能是返回值)。
在各处使用dyn Trait以获得接口的通用性,即使在严格的循环中,也可以用泛型来解决。
优点:
缺点:
在内部逻辑中使用静态调度,而在模块边界上仅使用dyn Trait。
优点:
缺点: