8.4 特征对象与动态分发
在 Rust 中,泛型通常通过静态分发(monomorphization)实现:编译器为每个具体类型生成独立的代码,从而获得零成本抽象。然而,这种机制要求所有类型在编译时已知,无法处理运行时才确定的异构类型集合。为此,Rust 提供了特征对象(Trait Objects),支持动态分发(dynamic dispatch)。
什么是特征对象
特征对象是一种引用形式,如 &dyn Trait 或 Box<dyn Trait>,它指向一个实现了指定 trait 的具体值,同时携带一个虚表(vtable),用于在运行时查找方法实现。
例如:
trait Draw {
fn draw(&self);
}
struct Circle;
struct Square;
impl Draw for Circle {
fn draw(&self) { println!("Drawing a circle"); }
}
impl Draw for Square {
fn draw(&self) { println!("Drawing a square"); }
}
我们可以将不同类型的实例放入同一个集合中:
let circle = Circle;
let square = Square;
let shapes: Vec<&dyn Draw> = vec![&circle, &square];
for shape in shapes {
shape.draw(); // 运行时决定调用哪个 draw 实现
}
这里,&dyn Draw 就是一个特征对象。它不关心底层是 Circle 还是 Square,只要实现了 Draw 即可。
动态分发 vs 静态分发
| 特性 | 静态分发(泛型) | 动态分发(特征对象) |
|---|---|---|
| 分发时机 | 编译时 | 运行时 |
| 性能 | 零开销,可内联 | 有虚表查找和间接调用开销 |
| 二进制大小 | 可能膨胀(每种类型一份代码) | 代码共享 |
| 类型灵活性 | 必须同类型或通过泛型约束 | 支持异构类型集合 |
| 是否知道具体类型 | 是 | 否 |
特征对象的限制:对象安全性
并非所有 trait 都能作为特征对象使用。只有满足对象安全(object safety)条件的 trait 才可以。主要规则包括:
- 所有方法不能返回
Self; - 不能包含泛型参数;
- 不能使用
Sized约束(除非显式放宽)。
例如,以下 trait 不是对象安全的:
trait Cloneable {
fn clone(&self) -> Self; // 返回 Self → 不对象安全
}
因为运行时无法知道 Self 的大小和类型,无法分配内存。
标准库中的 Clone、Sized 等 trait 因此不能直接用作特征对象。
使用 Box<dyn Trait> 转移所有权
当需要将值的所有权存入集合或返回特征对象时,常用 Box<dyn Trait>:
fn make_shape(kind: &str) -> Box<dyn Draw> {
match kind {
"circle" => Box::new(Circle),
"square" => Box::new(Square),
_ => panic!("Unknown shape"),
}
}
let shape = make_shape("circle");
shape.draw();
Box 在堆上分配内存,使特征对象拥有稳定地址和统一大小(指针大小 + vtable 指针)。
特征对象与生命周期
特征对象可包含生命周期标注:
fn draw_shapes<'a>(shapes: Vec<&'a dyn Draw>) {
for s in shapes { s.draw(); }
}
若未显式标注,Rust 会根据上下文推断,但有时需手动指定以避免歧义。
小结
特征对象是 Rust 实现运行时多态的核心机制,适用于需要处理多种不同类型但共享同一行为接口的场景,如 GUI 组件、插件系统或事件处理器。尽管存在轻微性能开销,但它提供了静态分发无法实现的灵活性。理解其工作原理、适用条件及对象安全限制,有助于在合适的地方选择动态分发,构建既灵活又安全的系统架构。