泛型在编译期选择实现,trait object 在运行时选择。这篇把 dyn Trait 拆开看:它为什么只能放在指针后面、胖指针里装了什么、vtable 里记着什么,以及它和泛型在代码生成上的差别。
源码:
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 字节 源码:
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 / 1vtable 的布局不是语言保证的稳定接口,这里只是在当前编译器版本上观察。胖指针的更多内容见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 的对象安全。