跳到主要内容

Trait Object 与动态分发

问题

Rust 中静态分发和动态分发有什么区别?Trait Object 的底层是如何实现的?

答案

静态分发 vs 动态分发

trait Shape {
fn area(&self) -> f64;
}

struct Circle { radius: f64 }
struct Rect { w: f64, h: f64 }

impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Shape for Rect {
fn area(&self) -> f64 { self.w * self.h }
}

// 静态分发:编译时确定类型,单态化
fn print_area_static(shape: &impl Shape) {
println!("面积: {}", shape.area());
}

// 动态分发:运行时通过 vtable 查找方法
fn print_area_dynamic(shape: &dyn Shape) {
println!("面积: {}", shape.area());
}
特性静态分发动态分发
语法impl Trait / T: Traitdyn Trait
类型确定时机编译时运行时
性能零开销,可内联vtable 间接调用
二进制大小膨胀(每个类型一份)共享一份代码
异构集合

Trait Object 内存布局

&dyn Trait 是一个胖指针,包含两个指针:

// &dyn Shape 的内部结构(伪代码)
struct TraitObject {
data: *const (), // 指向具体数据
vtable: *const (), // 指向虚表
}

vtable 包含:

  • drop 函数指针(析构)
  • 数据的 sizealign
  • Trait 中每个方法的函数指针

Trait Object 的使用方式

// 1. 引用形式
fn draw(shape: &dyn Shape) { }

// 2. Box 智能指针(拥有所有权)
fn create_shape(kind: &str) -> Box<dyn Shape> {
match kind {
"circle" => Box::new(Circle { radius: 1.0 }),
"rect" => Box::new(Rect { w: 2.0, h: 3.0 }),
_ => panic!("unknown"),
}
}

// 3. 异构集合
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rect { w: 2.0, h: 3.0 }),
];
for shape in &shapes {
println!("{}", shape.area());
}

对象安全(Object Safety)

并非所有 Trait 都能用作 Trait Object,必须满足对象安全规则:

// ✅ 对象安全
trait Draw {
fn draw(&self);
}

// ❌ 不对象安全:有泛型方法
trait Serialize {
fn serialize<W: Write>(&self, writer: &mut W);
// 泛型方法无法放入 vtable(无限种可能)
}

// ❌ 不对象安全:方法返回 Self
trait Clone {
fn clone(&self) -> Self;
// 不知道 Self 的具体大小
}

// ❌ 不对象安全:有 Sized 约束
trait Foo: Sized {
fn foo(&self);
}

对象安全的核心要求:

  1. 方法的 receiver 必须是 &self&mut selfself: Box<Self>
  2. 方法不能有泛型类型参数
  3. 方法不能返回 Self(除非通过 where Self: Sized 排除)
  4. Trait 本身不能有 Self: Sized 约束
绕过限制

可以用 where Self: Sized 将特定方法排除出 Trait Object:

trait MyTrait {
fn normal_method(&self);

// 这个方法不会出现在 vtable 中
fn generic_method<T>(&self, t: T) where Self: Sized;
}

常见面试问题

Q1: &dyn TraitBox<dyn Trait> 的区别?

答案

特性&dyn TraitBox<dyn Trait>
所有权借用拥有
生命周期需要标注独立(拥有数据)
内存胖指针(2 × usize)胖指针 + 堆分配
适用场景临时使用存储、传递所有权
// 引用:借用,不拥有
fn process(shape: &dyn Shape) { }

// Box:拥有所有权,可以存到集合、跨函数传递
fn create() -> Box<dyn Shape> { Box::new(Circle { radius: 1.0 }) }

Q2: 为什么 Clone 不是对象安全的?

答案

Clone trait 的 clone() 方法返回 Self,但 Trait Object 不知道 Self 的具体类型和大小,无法在栈上分配返回值。

解决方案——使用 dyn_clone 之类的 crate,或自定义 BoxClone

trait CloneBox {
fn clone_box(&self) -> Box<dyn CloneBox>;
}

impl<T: Clone + 'static> CloneBox for T {
fn clone_box(&self) -> Box<dyn CloneBox> {
Box::new(self.clone())
}
}

Q3: 动态分发的性能开销有多大?

答案

主要开销来自两方面:

  1. 间接调用:通过 vtable 指针查表 + 函数指针调用,比直接调用多一次内存访问
  2. 无法内联:编译器无法将动态分发的方法调用内联优化

实测通常只有个位数纳秒级差异。在大多数业务场景中可以忽略不计,但在紧密循环中(如每秒调用百万次)可能可测量。

Q4: dyn Trait + Send + Sync 是什么意思?

答案

Trait Object 可以附加自动 Trait(auto trait)约束:

// 可以跨线程发送的 Trait Object
fn spawn_task(task: Box<dyn FnOnce() + Send + 'static>) {
std::thread::spawn(task);
}

// 可以跨线程共享的 Trait Object
type SharedService = Arc<dyn Service + Send + Sync>;

Send + Sync 确保 Trait Object 背后的具体类型是线程安全的。

Q5: Trait upcasting 是什么?

答案

Rust 1.76+ 支持 Trait upcasting——将子 Trait Object 转换为父 Trait Object:

trait Base { fn base_method(&self); }
trait Derived: Base { fn derived_method(&self); }

fn upcast(d: &dyn Derived) {
let b: &dyn Base = d; // ✅ Rust 1.76+ 支持
b.base_method();
}

在此之前需要手动添加 as_base() 方法来转换。

相关链接