本节摘要:
dyn Trait把"任何履行了契约的类型"装进一个引用或 Box,方法调用在运行期通过虚表派发;泛型在编译期展开、Trait 对象在运行期查表,两者各有辖区。读完你能在"异构集合""插件式接口"等场景选对多态方案。
let shapes: Vec<Vault<u32>> = ...; // 泛型容器只能装同一种 T
要一个保管室同时装纸质卷宗与电子卷宗,泛型无解——容器里每个元素类型不同。Trait 对象出庭:
trait Exhibit { fn describe(&self) -> String; } struct Finger { id: u8 } struct Photo { size: u32 } impl Exhibit for Finger { fn describe(&self) -> String { format!("指纹 {}", self.id) } } impl Exhibit for Photo { fn describe(&self) -> String { format!("照片 {}KB", self.size) } } let locker: Vec<Box<dyn Exhibit>> = vec![ Box::new(Finger { id: 7 }), Box::new(Photo { size: 2048 }), ]; for e in &locker { println!("{}", e.describe()); }
dyn Exhibit 是胖指针:数据指针加虚表指针。调用 describe 时查虚表找到具体实现——这就是动态派发,与 C++ 虚函数同机制。
| 维度 | 泛型(静态派发) | dyn Trait(动态派发) |
|---|---|---|
| 派发时机 | 编译期 | 运行期 |
| 性能 | 直接调用,可内联 | 一次虚表查取,禁止内联 |
| 异构集合 | 不支持 | 支持 |
| 二进制体积 | 每类型一份代码膨胀 | 一份 |
| 编译边界 | 需见到具体类型 | 可跨动态库分发 |
| 约束 | 任意 Trait | 仅对象安全 Trait |
对象安全的判据:方法不返回 Self、无泛型参数、不按值收 Self。带泛型方法的 Trait(如 Clone 的 clone 返回 Self)无法做对象——虚表条目无法定长。
⚠️ 常见坑:
Box<dyn Trait>默认带'static约束,想装借来的数据要写成Box<dyn Trait + 'a>。这是第 3.3 节法条在 Trait 对象上的适用。
&dyn Trait 与 Box<dyn Trait> 都占两个机器字:一指数据,一指 vtable。派发成本是一次间接寻址加一次边界无法内联的调用,与 C++ 的虚函数同量级。
trait Notify { fn send(&self) -> String; } struct Sms { to: String } struct Pager { level: u8 } impl Notify for Sms { fn send(&self) -> String { format!("短信→{}", self.to) } } impl Notify for Pager { fn send(&self) -> String { format!("寻呼 P{} 级", self.level) } } fn blast(channels: &[Box<dyn Notify>]) { for c in channels { println!("{}", c.send()); // 运行期查 vtable 找到 Sms::send 或 Pager::send } } fn main() { let cs: Vec<Box<dyn Notify>> = vec![ Box::new(Sms { to: String::from("值班室") }), Box::new(Pager { level: 2 }), ]; blast(&cs); // 输出:短信→值班室 / 寻呼 P2 级 }
对比泛型的静态派发:泛型在编译期为每个类型展开专属代码,可内联、零间接,代价是代码膨胀与编译时长;trait 对象一份代码处理所有类型,代价是间接调用与放弃内联。选择判据是"类型集合在编译期是否已知全部成员"——插件式架构、事件总线选 dyn,库内算法选泛型。
不是所有 trait 都能做 dyn 对象,历史上称 object safety。驳回理由各有编号可循。
trait Cloneable: Clone { } // 驳回:Clone 要求 Self: Sized // fn make(c: &dyn Cloneable) {} // E0038:trait 不 dyn 兼容 trait Static { fn make() -> Self; // 驳回:无 receiver,无法从胖指针找到 Self fn tag(&self) -> &'static str where Self: Sized { "S" } // 合规:where 子句豁免 } trait Ok { fn name(&self) -> String; // 兼容:仅 &self 方法 }
三条主要驳回理由:方法返回 Self 且无 Sized 豁免、方法含泛型参数(vtable 无法穷举实例)、trait 要求 Sized。关联类型允许存在但使用 dyn Trait<Item = X> 时必须写明。E0038 报错原文会列出驳回的具体方法,照单修改即可。
dyn 对象丢失了具体类型信息,需要取回时有 std::any 的法定程序。
use std::any::Any; fn probe(v: &dyn Any) { if let Some(s) = v.downcast_ref::<String>() { println!("字符串:{}", s); } else if let Some(n) = v.downcast_ref::<u64>() { println!("整数:{}", n); } else { println!("未识别类型"); } } fn main() { probe(&String::from("证物")); probe(&173u64); }
downcast_ref 返回 Option,转型失败是正常返回值。把 Any 当"万能抽屉"滥用会让类型系统退化为动态检查,正确场合是错误类型的_errno 式携带、测试工具、序列化层这类确实必须跨类型容器的边界。