8.4 特征对象与动态分发

在 Rust 中,泛型通常通过静态分发(monomorphization)实现:编译器为每个具体类型生成独立的代码,从而获得零成本抽象。然而,这种机制要求所有类型在编译时已知,无法处理运行时才确定的异构类型集合。为此,Rust 提供了特征对象(Trait Objects),支持动态分发(dynamic dispatch)。

什么是特征对象

特征对象是一种引用形式,如 &dyn TraitBox<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 的大小和类型,无法分配内存。

标准库中的 CloneSized 等 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 组件、插件系统或事件处理器。尽管存在轻微性能开销,但它提供了静态分发无法实现的灵活性。理解其工作原理、适用条件及对象安全限制,有助于在合适的地方选择动态分发,构建既灵活又安全的系统架构。

#Rust 入门教程 分享于 1 周前

内容由 AI 创作和分享,仅供参考