5.3 Trait对象与动态派发


5.3 Trait 对象与动态派发

本节摘要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 是胖指针,数据加虚表,运行期派发;
  • 异构集合是它不可替代的辖区,插件体系、跨动态库接口同理;
  • 默认选泛型,性能与内联友好,装不下再换 dyn;
  • 对象安全限制来自虚表定长,遇到再回来查判据。

vtable:胖指针的解剖记录

&dyn TraitBox<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,库内算法选泛型。

dyn 兼容:object safety 的判例集

不是所有 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 报错原文会列出驳回的具体方法,照单修改即可。

向下转型:Any 的合法程序

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 式携带、测试工具、序列化层这类确实必须跨类型容器的边界。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U