bigywater.tech
Rust · 39

Trait Object 与动态分发

泛型在编译期选择实现,trait object 在运行时选择。这篇把 dyn Trait 拆开看:它为什么只能放在指针后面、胖指针里装了什么、vtable 里记着什么,以及它和泛型在代码生成上的差别。

16 字节
&dyn Trait / Box 的大小(数据指针 + vtable 指针)
8 / 40 / 0
从 vtable 里读出的 Circle / Big / Unit 的 size,与 size_of 一致
1 vs 2
&dyn 函数与泛型函数(两个类型)生成的函数个数

动态分发

实测 · 本机执行输出
  源码:
      1 | trait Shape { fn area(&self) -> f64; fn name(&self) -> &'static str; }
      2 | struct Circle(f64); struct Rect(f64, f64); struct Tri(f64, f64);
      3 | impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } fn name(&self) -> &'static str { "圆" } }
      4 | impl Shape for Rect { fn area(&self) -> f64 { self.0 * self.1 } fn name(&self) -> &'static str { "矩形" } }
      5 | impl Shape for Tri { fn area(&self) -> f64 { 0.5 * self.0 * self.1 } fn name(&self) -> &'static str { "三角形" } }
      6 | fn main() {
      7 |     let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Rect(2.0, 3.0)), Box::new(Tri(4.0, 5.0))];
      8 |     for s in &shapes { println!("{} 面积 {:.2}", s.name(), s.area()); }
      9 |     println!("总面积 {:.2}", shapes.iter().map(|s| s.area()).sum::<f64>());
     10 | }
  编译:成功
  运行(退出码 0):
    圆 面积 3.14
    矩形 面积 6.00
    三角形 面积 10.00
    总面积 19.14

不定大小与胖指针

实测 · 本机执行输出
  源码:
      1 | trait T { fn f(&self) {} }
      2 | struct A; impl T for A {}
      3 | fn main() { let x: dyn T = A; }
  编译:失败
    error[E0308]: mismatched types
     --> <实验目录>/src/u39b.rs:3:28
      |
    3 | fn main() { let x: dyn T = A; }
      |                    -----   ^ expected `dyn T`, found `A`
      |                    |
      |                    expected due to this
      |
      = note: expected trait object `dyn T`
                       found struct `A`
      = help: `A` implements `T` so you could box the found value and coerce it to the trait object `Box<dyn T>`, you will have to change the expected type as well
    
    error[E0277]: the size for values of type `dyn T` cannot be known at compilation time
     --> <实验目录>/src/u39b.rs:3:17
      |
    3 | fn main() { let x: dyn T = A; }
      |                 ^ doesn't have a size known at compile-time
      |
      = help: the trait `Sized` is not implemented for `dyn T`
      = note: all local variables must have a statically known size
    help: consider borrowing here
      |
    3 | fn main() { let x: &dyn T = A; }
      |                    +
    …(共 29 行)
  编译:成功
  运行(退出码 0):
    &Circle(普通引用)    = 8 字节
    &dyn Shape(胖指针)   = 16 字节
    Box<dyn Shape>       = 16 字节
    Box<f64>             = 8 字节
    Rc<dyn Shape>        = 16 字节

vtable 里有什么

实测 · 本机执行输出
  源码:
      1 | use std::mem::{size_of, align_of, transmute};
      2 | trait Shape { fn area(&self) -> f64; }
      3 | struct Circle(f64);                  // 8 字节
      4 | struct Big { a: [u64; 5] }           // 40 字节
      5 | struct Unit;                          // 0 字节
      6 | impl Shape for Circle { fn area(&self) -> f64 { self.0 } }
      7 | impl Shape for Big { fn area(&self) -> f64 { self.a[0] as f64 } }
      8 | impl Shape for Unit { fn area(&self) -> f64 { 0.0 } }
      9 | fn parts(s: &dyn Shape) -> (usize, usize) { unsafe { transmute::<&dyn Shape, (usize, usize)>(s) } }
     10 | fn vt_size_align(vt: usize) -> (usize, usize) { let p = vt as *const usize; unsafe { (*p.add(1), *p.add(2)) } }   // 观察到的布局:[drop_in_place, size, align, 方法...]
     11 | fn main() {
     12 |     let (c1, c2, b, u) = (Circle(1.0), Circle(2.0), Big { a: [1; 5] }, Unit);
     13 |     let (d1, v1) = parts(&c1); let (d2, v2) = parts(&c2); let (_d3, v3) = parts(&b); let (_d4, v4) = parts(&u);
     14 |     println!("Circle 的两个对象:数据地址不同 {},vtable 地址相同 {}", d1 != d2, v1 == v2);
     15 |     println!("Circle 与 Big 的 vtable 不同:{};Circle 与 Unit 的 vtable 不同:{}", v1 != v3, v1 != v4);
     16 |     for (name, vt, sz, al) in [("Circle", v1, size_of::<Circle>(), align_of::<Circle>()), ("Big", v3, size_of::<Big>(), align_of::<Big>()), ("Unit", v4, size_of::<Unit>(), align_of::<Unit>())] {
     17 |         let (s, a) = vt_size_align(vt);
     18 |         println!("{:<7} vtable 里读出 size = {:>2}, align = {};size_of / align_of = {} / {}", name, s, a, sz, al);
     19 |     }
     20 | }
  编译:成功
  运行(退出码 0):
    Circle 的两个对象:数据地址不同 true,vtable 地址相同 true
    Circle 与 Big 的 vtable 不同:true;Circle 与 Unit 的 vtable 不同:true
    Circle  vtable 里读出 size =  8, align = 8;size_of / align_of = 8 / 8
    Big     vtable 里读出 size = 40, align = 8;size_of / align_of = 40 / 8
    Unit    vtable 里读出 size =  0, align = 1;size_of / align_of = 0 / 1

vtable 的布局不是语言保证的稳定接口,这里只是在当前编译器版本上观察。胖指针的更多内容见Trait Object 的胖指针。

与泛型对比

实测 · 本机执行输出
  total_dyn(&dyn Shape) 生成的函数个数:1;total_gen<S: Shape> 生成的函数个数(A、B 各一份):2
  use_all 里的四次调用依次指向: ['total_dyn', 'total_dyn', 'total_gen::<A>', 'total_gen::<B>']

性能对比见静态分发与动态分发性能对比;哪些 trait 能做成对象见dyn Trait 的对象安全。

分步演示

trait object
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。vtable 读取依赖当前编译器的内部布局(unsafe,仅用于观察),在其它版本可能失效
    • 没有验证:dyn Trait + Send、trait upcasting、dyn Any 的向下转型、多个 trait 组合的对象、生命周期对 dyn 的影响(Box)
    京ICP备2026067502号-1京公网安备11010502063487号