目录
- 一、先拆开看:Box 和 dyn Trait
- 二、一个完整的 Box<dyn Trait> 示例
- 三、Box<dyn Trait> 最大的作用:保存不同类型的对象
- 四、为什么不能直接使用 dyn Animal
- 五、Box<dyn Trait> 在内存中是什么结构
- 六、动态分发和静态分发的区别
- 七、Box<dyn Trait> 和 &dyn Trait 的区别
- 八、实际应用一:策略模式
- 九、实际应用二:返回不同的具体类型
- 十、impl Trait 和 dyn Trait 不一样
- 十一、实际应用三:保存不同类型的闭包
- 十二、实际应用四:统一不同错误类型
- 十三、Box<dyn Trait + Send + Sync> 是什么意思
- 十四、不是所有 trait 都能写成 dyn Trait
- 十五、Box<dyn Trait> 默认生命周期问题
- 十六、如何取回原来的具体类型
- 十七、多个 trait 限制怎么写
- 十八、Box<dyn Trait> 的所有权行为
- 十九、什么时候适合使用 Box<dyn Trait>
- 二十、什么时候不建议使用
- 二十一、一个贴近 Web 项目的案例
- 二十二、最终理解
Box<dyn Trait> 是 Rust 中非常常见的“堆对象 + 接口类型 + 运行时多态”写法。
例如:
Box<dyn Animal>
可以理解为:
在堆上保存一个实现了
Animaltrait 的具体对象,但外部不关心它究竟是Dog、Cat还是其他类型,只通过Animal提供的方法操作它。
这有点类似 Java:
Animal animal = new Dog();
但 Rust 会明确表达:
Box:拥有对象,并将对象放到堆上;dyn Animal:这个对象的具体类型在编译期不确定,只知道它实现了Animal;- 调用方法时通过运行时动态分发找到正确的方法。
一、先拆开看:Box 和 dyn Trait
1. Box<T>
Box<T> 是 Rust 的智能指针,用于:
- 把数据放到堆上;
- 拥有这块数据;
- 离开作用域后自动释放。
fn main() {
let value: Box<i32> = Box::new(100);
println!("{}", value);
}
内存结构大致如下:
栈:
value ──────────────┐
│
堆: ▼
100
value 本身是一个指针,真正的 100 存储在堆中。
2. dyn Trait
dyn Trait 表示“trait 对象”。
例如:
trait Animal {
fn speak(&self);
}
那么:
dyn Animal
表示:
某个实现了
Animaltrait 的对象,但不知道它具体是哪一种类型。
注意,不能直接这样写:
let animal: dyn Animal;
因为 dyn Animal 的具体大小在编译期未知。
必须放到某种指针后面:
Box<dyn Animal> &dyn Animal Rc<dyn Animal> Arc<dyn Animal>
二、一个完整的 Box<dyn Trait> 示例
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
}
struct Dog {
name: String,
}
struct Cat {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("{}:汪汪汪!", self.name);
}
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("{}:喵喵喵!", self.name);
}
}
fn main() {
let dog: Box<dyn Animal> = Box::new(Dog {
name: String::from("旺财"),
});
let cat: Box<dyn Animal> = Box::new(Cat {
name: String::from("小花"),
});
dog.speak();
cat.speak();
println!("dog 的名字:{}", dog.name());
println!("cat 的名字:{}", cat.name());
}
输出:
旺财:汪汪汪! 小花:喵喵喵! dog 的名字:旺财 cat 的名字:小花
这里:
Box::new(Dog { ... })
原始类型是:
Box<Dog>
但它可以自动转换为:
Box<dyn Animal>
因为 Dog 实现了 Animal。
同理:
Box<Cat>
也可以转换成:
Box<dyn Animal>
三、Box<dyn Trait> 最大的作用:保存不同类型的对象
普通的 Vec<T> 要求所有元素类型完全一致。
例如下面是不允许的:
struct Dog;
struct Cat;
fn main() {
let animals = vec![
Dog,
Cat,
];
}
因为 Dog 和 Cat 是不同类型。
但是它们都实现了同一个 trait 时,可以使用:
Vec<Box<dyn Animal>>
完整示例:
trait Animal {
fn speak(&self);
}
struct Dog {
name: String,
}
struct Cat {
name: String,
}
struct Bird {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("狗 {}:汪汪!", self.name);
}
}
impl Animal for Cat {
fn speak(&self) {
println!("猫 {}:喵喵!", self.name);
}
}
impl Animal for Bird {
fn speak(&self) {
println!("鸟 {}:叽叽!", self.name);
}
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog {
name: String::from("旺财"),
}),
Box::new(Cat {
name: String::from("小花"),
}),
Box::new(Bird {
name: String::from("小黄"),
}),
];
for animal in &animals {
animal.speak();
}
}
输出:
狗 旺财:汪汪! 猫 小花:喵喵! 鸟 小黄:叽叽!
虽然 Vec 中实际放了三种不同类型:
Dog Cat Bird
但从 Vec 的角度看,它们全都是:
Box<dyn Animal>
这就是运行时多态。
四、为什么不能直接使用 dyn Animal
1. 不同实现类型的大小可能不同
例如:
struct Dog {
name: String,
}
struct Bird {
age: u8,
}
Dog 和 Bird 的大小不同。
假设:
Dog 可能占 24 字节 Bird 可能占 1 字节
当你写:
dyn Animal
编译器只知道对象实现了 Animal,但不知道对象究竟是:
Dog Cat Bird
因此不知道需要分配多少栈空间。
所以这不成立:
let animal: dyn Animal;
但使用 Box 后,栈上的 Box 指针大小是可知的,真正大小不确定的对象放在堆上:
let animal: Box<dyn Animal>;
五、Box<dyn Trait> 在内存中是什么结构
Box<dyn Animal> 并不是一个普通的单指针,它通常是一个“胖指针”。
大致包含两个部分:
Box<dyn Animal> ├── 数据指针:指向堆上的 Dog、Cat 等具体对象 └── 虚表指针:指向该类型对应的方法表
例如:
let animal: Box<dyn Animal> = Box::new(Dog {
name: "旺财".to_string(),
});
内存结构可以理解为:
栈:
animal
┌───────────────────────────┐
│ data_ptr ────────────────┼──────┐
│ vtable_ptr ───────────────┼───┐ │
└───────────────────────────┘ │ │
│ │
虚表: │ │
┌───────────────────────────┐ │ │
│ Dog::speak │◀──┘ │
│ Dog::name │ │
│ Dog 的大小 │ │
│ Dog 的对齐信息 │ │
│ Dog 的析构函数 │ │
└───────────────────────────┘ │
│
堆: │
┌───────────────────────────┐ │
│ Dog │◀─────┘
│ name: "旺财" │
└───────────────────────────┘
调用:
animal.speak();
底层逻辑类似:
1. 通过 animal 的虚表指针找到 Dog::speak 2. 将 animal 的数据指针传给 Dog::speak 3. 执行 Dog 对应的方法
这叫做:
动态分发 Dynamic Dispatch
六、动态分发和静态分发的区别
Rust 中常见的多态有两种。
1. 泛型:静态分发
trait Animal {
fn speak(&self);
}
fn make_speak<T: Animal>(animal: &T) {
animal.speak();
}
也可以写成:
fn make_speak(animal: &impl Animal) {
animal.speak();
}
编译器会针对每种具体类型生成代码:
make_speak::<Dog>() make_speak::<Cat>()
相当于:
Dog 调用 Dog::speak Cat 调用 Cat::speak
方法在编译期就确定了。
优点:
- 调用速度快;
- 容易内联;
- 没有虚表调用开销。
缺点:
- 不容易在一个集合中存放多种类型;
- 每种类型会生成不同版本的函数,可能增加二进制体积。
2. dyn Trait:动态分发
fn make_speak(animal: &dyn Animal) {
animal.speak();
}
或者:
fn make_speak(animal: Box<dyn Animal>) {
animal.speak();
}
方法在运行时通过虚表确定。
优点:
- 可以隐藏具体类型;
- 可以在一个集合中保存不同类型;
- 适合插件、回调、策略模式、依赖注入等场景。
缺点:
- 有一次虚表间接调用;
- 某些优化和内联比较困难;
- trait 必须满足动态兼容要求。
七、Box<dyn Trait> 和 &dyn Trait 的区别
&dyn Trait:借用对象
fn print_animal(animal: &dyn Animal) {
animal.speak();
}
调用:
let dog = Dog {
name: "旺财".to_string(),
};
print_animal(&dog);
特点:
- 不拥有对象;
- 不负责释放对象;
- 只是临时借用;
- 对象可能在栈上,也可能在堆上。
Box<dyn Trait>:拥有对象
fn consume_animal(animal: Box<dyn Animal>) {
animal.speak();
}
调用:
let dog = Box::new(Dog {
name: "旺财".to_string(),
});
consume_animal(dog);
特点:
- 拥有对象;
- 对象存储在堆上;
Box离开作用域时自动释放对象;- 可以把所有权移动到其他地方。
简单比较:
| 类型 | 是否拥有对象 | 对象位置 | 适用场景 |
|---|---|---|---|
&dyn Trait |
否 | 不确定 | 临时借用 |
&mut dyn Trait |
否 | 不确定 | 临时可变借用 |
Box<dyn Trait> |
是 | 堆 | 独占所有权 |
Rc<dyn Trait> |
是,共享 | 堆 | 单线程共享 |
Arc<dyn Trait> |
是,共享 | 堆 | 多线程共享 |
八、实际应用一:策略模式
假设一个支付系统支持:
- 支付宝支付;
- 微信支付;
- 银行卡支付。
定义统一支付接口:
trait Payment {
fn pay(&self, amount: f64);
}
struct Alipay;
struct WechatPay;
struct BankCard {
card_number: String,
}
impl Payment for Alipay {
fn pay(&self, amount: f64) {
println!("使用支付宝支付:{amount:.2} 元");
}
}
impl Payment for WechatPay {
fn pay(&self, amount: f64) {
println!("使用微信支付:{amount:.2} 元");
}
}
impl Payment for BankCard {
fn pay(&self, amount: f64) {
println!(
"使用银行卡 {} 支付:{amount:.2} 元",
self.card_number
);
}
}
struct OrderService {
payment: Box<dyn Payment>,
}
impl OrderService {
fn new(payment: Box<dyn Payment>) -> Self {
Self { payment }
}
fn checkout(&self, amount: f64) {
println!("开始结算订单");
self.payment.pay(amount);
println!("订单结算完成");
}
}
fn main() {
let service = OrderService::new(Box::new(Alipay));
service.checkout(199.0);
let service = OrderService::new(Box::new(BankCard {
card_number: "6222****8888".to_string(),
}));
service.checkout(299.0);
}
这里:
struct OrderService {
payment: Box<dyn Payment>,
}
表示 OrderService 不关心具体支付方式,只依赖 Payment 接口。
后面增加新的支付方式时,不需要修改 OrderService:
struct Paypal;
impl Payment for Paypal {
fn pay(&self, amount: f64) {
println!("使用 PayPal 支付:{amount:.2} 元");
}
}
直接注入:
let service = OrderService::new(Box::new(Paypal));
这也是一种依赖注入。
九、实际应用二:返回不同的具体类型
假设根据配置返回不同日志记录器:
trait Logger {
fn log(&self, message: &str);
}
struct ConsoleLogger;
struct FileLogger {
filename: String,
}
impl Logger for ConsoleLogger {
fn log(&self, message: &str) {
println!("[控制台] {message}");
}
}
impl Logger for FileLogger {
fn log(&self, message: &str) {
println!("[文件 {}] {message}", self.filename);
}
}
fn create_logger(use_file: bool) -> Box<dyn Logger> {
if use_file {
Box::new(FileLogger {
filename: "app.log".to_string(),
})
} else {
Box::new(ConsoleLogger)
}
}
fn main() {
let logger = create_logger(true);
logger.log("系统启动成功");
}
为什么这里需要 Box<dyn Logger>?
因为两个分支返回的具体类型不同:
FileLogger ConsoleLogger
下面这样是不允许的:
fn create_logger(use_file: bool) -> impl Logger {
if use_file {
FileLogger {
filename: "app.log".to_string(),
}
} else {
ConsoleLogger
}
}
impl Logger 虽然隐藏具体类型,但一个函数最终只能返回一种具体类型。
而:
Box<dyn Logger>
可以在运行时保存不同的实现类型。
十、impl Trait 和 dyn Trait 不一样
很多初学者会把它们混在一起。
impl Trait
fn create_animal() -> impl Animal {
Dog
}
意思是:
返回某一个确定的、实现了
Animal的具体类型,只是不告诉调用者具体类型名称。
这个具体类型在编译时是确定的。
不能一个分支返回 Dog,另一个分支返回 Cat:
fn create_animal(flag: bool) -> impl Animal {
if flag {
Dog
} else {
Cat
}
}
除非 Dog 和 Cat 实际上是同一个类型,否则编译失败。
dyn Trait
fn create_animal(flag: bool) -> Box<dyn Animal> {
if flag {
Box::new(Dog)
} else {
Box::new(Cat)
}
}
意思是:
返回的具体类型可以在运行时变化,只要它实现了
Animal。
十一、实际应用三:保存不同类型的闭包
每个闭包都有一个独立的匿名类型。
例如:
let callback1 = || println!("任务一");
let number = 100;
let callback2 = move || println!("任务二:{number}");
这两个闭包的类型不同,不能直接放到普通 Vec 里。
可以使用:
Vec<Box<dyn Fn()>>
示例:
fn main() {
let name = String::from("Albert");
let number = 100;
let callbacks: Vec<Box<dyn Fn()>> = vec![
Box::new(|| {
println!("执行普通任务");
}),
Box::new(move || {
println!("你好,{name}");
}),
Box::new(move || {
println!("当前数字:{number}");
}),
];
for callback in callbacks {
callback();
}
}
Fn 本身也是一个 trait。
因此:
Box<dyn Fn()>
就是:
一个拥有闭包的堆指针,这个闭包可以被调用,参数为空,返回值为空。
还可以写:
Box<dyn Fn(i32) -> i32>
示例:
fn main() {
let operations: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|value| value + 10),
Box::new(|value| value * 2),
Box::new(|value| value * value),
];
for operation in operations {
println!("{}", operation(5));
}
}
输出:
15 10 25
十二、实际应用四:统一不同错误类型
Rust 中经常看到:
Result<T, Box<dyn std::error::Error>>
例如:
use std::error::Error;
use std::fs;
fn read_number() -> Result<i32, Box<dyn Error>> {
let content = fs::read_to_string("number.txt")?;
let number = content.trim().parse::<i32>()?;
Ok(number)
}
fn main() {
match read_number() {
Ok(number) => println!("数字是:{number}"),
Err(error) => println!("发生错误:{error}"),
}
}
这里可能出现不同错误:
std::io::Error std::num::ParseIntError
它们都实现了:
std::error::Error
所以统一转换成:
Box<dyn Error>
这样一个函数就可以返回多种不同的错误类型。
在异步或多线程项目中,经常使用:
Box<dyn Error + Send + Sync>
完整类型可能是:
Result<T, Box<dyn Error + Send + Sync + 'static>>
十三、Box<dyn Trait + Send + Sync> 是什么意思
例如:
Box<dyn Task + Send + Sync>
拆开来看:
Box └── 拥有一个堆对象 dyn Task └── 对象实现了 Task trait Send └── 对象可以安全地移动到其他线程 Sync └── 对象可以安全地被多个线程共享
示例:
trait Task {
fn execute(&self);
}
struct EmailTask {
recipient: String,
}
impl Task for EmailTask {
fn execute(&self) {
println!("发送邮件给:{}", self.recipient);
}
}
fn main() {
let task: Box<dyn Task + Send + Sync> = Box::new(EmailTask {
recipient: "user@example.com".to_string(),
});
let handle = std::thread::spawn(move || {
task.execute();
});
handle.join().unwrap();
}
由于对象需要被移动到新线程,trait 对象一般要带上:
Send
在多线程共享时还可能需要:
Sync
十四、不是所有 trait 都能写成 dyn Trait
要把 trait 用作 trait 对象,它必须满足“dyn compatible”,以前通常叫“对象安全”。
下面这个 trait 就不适合直接写成 dyn:
trait Creator {
fn create() -> Self;
fn clone_self(&self) -> Self;
fn convert<T>(&self, value: T);
}
存在几个问题:
fn create() -> Self;
没有 self 参数,并返回具体的 Self。
fn clone_self(&self) -> Self;
返回具体的 Self。
fn convert<T>(&self, value: T);
方法本身有泛型参数。
对于:
Box<dyn Creator>
编译器不知道真正的 Self 是什么,也无法为无限多种 T 创建有限的虚表方法。
可以给部分方法加上 Self: Sized
trait Animal {
fn speak(&self);
fn create() -> Self
where
Self: Sized;
}
这样:
fn speak(&self)
可以通过 dyn Animal 调用。
而:
fn create() -> Self
只能由已知的具体类型调用,不能通过 dyn Animal 调用。
示例:
trait Animal {
fn speak(&self);
fn create() -> Self
where
Self: Sized;
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("汪汪!");
}
fn create() -> Self {
Dog
}
}
fn main() {
let dog = Dog::create();
let animal: Box<dyn Animal> = Box::new(dog);
animal.speak();
}
十五、Box<dyn Trait> 默认生命周期问题
很多情况下:
Box<dyn Trait>
会被理解为:
Box<dyn Trait + 'static>
也就是说,放入其中的对象不能包含短生命周期的借用。
例如,下面的类型内部借用了字符串:
trait Message {
fn content(&self) -> &str;
}
struct BorrowedMessage<'a> {
content: &'a str,
}
impl<'a> Message for BorrowedMessage<'a> {
fn content(&self) -> &str {
self.content
}
}
如果要返回它,需要明确生命周期:
fn create_message<'a>(
content: &'a str,
) -> Box<dyn Message + 'a> {
Box::new(BorrowedMessage { content })
}
完整示例:
trait Message {
fn content(&self) -> &str;
}
struct BorrowedMessage<'a> {
content: &'a str,
}
impl<'a> Message for BorrowedMessage<'a> {
fn content(&self) -> &str {
self.content
}
}
fn create_message<'a>(
content: &'a str,
) -> Box<dyn Message + 'a> {
Box::new(BorrowedMessage { content })
}
fn main() {
let text = String::from("这是一条借用消息");
let message = create_message(&text);
println!("{}", message.content());
}
这里:
Box<dyn Message + 'a>
表示:
这个 trait 对象至少在生命周期
'a内有效,它内部可以借用生命周期为'a的数据。
十六、如何取回原来的具体类型
通常使用 Box<dyn Trait> 后,只应该通过 trait 方法操作对象。
但特殊情况下,可以通过 Any 做向下转型。
use std::any::Any;
trait Animal {
fn speak(&self);
fn as_any(&self) -> &dyn Any;
}
struct Dog {
name: String,
}
struct Cat {
age: u8,
}
impl Animal for Dog {
fn speak(&self) {
println!("{}:汪汪!", self.name);
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl Animal for Cat {
fn speak(&self) {
println!("猫的年龄是 {} 岁", self.age);
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn main() {
let animal: Box<dyn Animal> = Box::new(Dog {
name: "旺财".to_string(),
});
animal.speak();
if let Some(dog) = animal.as_any().downcast_ref::<Dog>() {
println!("它确实是一只狗,名字是:{}", dog.name);
} else {
println!("它不是 Dog");
}
}
这里:
downcast_ref::<Dog>()
会检查 trait 对象内部是不是 Dog。
成功返回:
Some(&Dog)
失败返回:
None
不过不要过度使用向下转型。大量 downcast 往往说明 trait 设计得不够完整。
十七、多个 trait 限制怎么写
下面这种写法通常不允许直接使用两个普通 trait:
Box<dyn Read + Write>
可以定义一个组合 trait:
use std::io::{Read, Write};
trait ReadWrite: Read + Write {}
impl<T> ReadWrite for T
where
T: Read + Write,
{
}
然后:
Box<dyn ReadWrite>
但是 Send、Sync 这类自动 trait 可以直接附加:
Box<dyn ReadWrite + Send + Sync>
常见完整形式是:
Box<dyn Trait + Send + Sync + 'static>
十八、Box<dyn Trait> 的所有权行为
Box 拥有内部对象,因此默认不能随意复制。
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("汪汪!");
}
}
fn consume(animal: Box<dyn Animal>) {
animal.speak();
}
fn main() {
let animal: Box<dyn Animal> = Box::new(Dog);
consume(animal);
// 编译失败:animal 已经被移动
// animal.speak();
}
调用:
consume(animal);
会把 Box 的所有权移动到函数中。
函数结束后,对象被自动释放。
如果只想借用:
fn use_animal(animal: &dyn Animal) {
animal.speak();
}
调用:
use_animal(animal.as_ref()); animal.speak();
也可以依赖自动解引用:
use_animal(&*animal);
通常更推荐:
animal.as_ref()
十九、什么时候适合使用 Box<dyn Trait>
适合的场景包括:
- 一个集合中需要保存多种不同类型:
Vec<Box<dyn Plugin>>
- 函数可能返回不同的实现类型:
fn create_storage() -> Box<dyn Storage>
- 运行时根据配置选择不同策略:
Box<dyn Payment> Box<dyn Logger> Box<dyn Cache>
- 实现插件系统:
Vec<Box<dyn Plugin>>
- 保存不同类型的闭包:
Vec<Box<dyn Fn()>>
- 统一不同错误:
Box<dyn Error>
- 对外隐藏具体实现:
pub fn create_service() -> Box<dyn Service>
二十、什么时候不建议使用
如果类型在编译期明确,并且不需要混合不同类型,优先使用泛型:
fn process<T: Service>(service: &T)
而不是无条件使用:
fn process(service: &dyn Service)
如果类型种类固定且数量较少,也可以考虑 enum。
例如只允许两种日志类型:
enum Logger {
Console(ConsoleLogger),
File(FileLogger),
}
然后:
impl Logger {
fn log(&self, message: &str) {
match self {
Logger::Console(logger) => logger.log(message),
Logger::File(logger) => logger.log(message),
}
}
}
使用 enum 的优点:
- 不需要虚表动态分发;
- 不一定需要堆分配;
- 编译器知道所有可能类型;
- 模式匹配更明确。
使用 Box<dyn Trait> 的优点:
- 类型可以扩展;
- 调用方不必知道所有实现;
- 适合插件和依赖注入;
- 不需要每新增一个实现就修改枚举。
二十一、一个贴近 Web 项目的案例
例如你的 Rust Web 项目中有不同通知渠道:
trait NotificationSender {
fn send(&self, recipient: &str, content: &str);
}
struct EmailSender {
smtp_server: String,
}
struct SmsSender {
api_url: String,
}
struct WechatSender {
corp_id: String,
}
impl NotificationSender for EmailSender {
fn send(&self, recipient: &str, content: &str) {
println!(
"通过 SMTP {} 向 {} 发送邮件:{}",
self.smtp_server,
recipient,
content
);
}
}
impl NotificationSender for SmsSender {
fn send(&self, recipient: &str, content: &str) {
println!(
"通过短信接口 {} 向 {} 发送短信:{}",
self.api_url,
recipient,
content
);
}
}
impl NotificationSender for WechatSender {
fn send(&self, recipient: &str, content: &str) {
println!(
"通过企业微信 {} 向 {} 发送消息:{}",
self.corp_id,
recipient,
content
);
}
}
struct TicketService {
sender: Box<dyn NotificationSender + Send + Sync>,
}
impl TicketService {
fn new(
sender: Box<dyn NotificationSender + Send + Sync>,
) -> Self {
Self { sender }
}
fn create_ticket(&self, user: &str, title: &str) {
println!("创建工单:{title}");
self.sender.send(
user,
&format!("您的工单“{title}”已经创建成功"),
);
}
}
fn main() {
let sender = EmailSender {
smtp_server: "smtp.example.com".to_string(),
};
let service = TicketService::new(Box::new(sender));
service.create_ticket("albert@example.com", "电脑无法连接网络");
}
未来想改成企业微信通知,只需要修改注入对象:
let sender = WechatSender {
corp_id: "ww123456".to_string(),
};
let service = TicketService::new(Box::new(sender));
TicketService 本身不需要修改。
二十二、最终理解
看到:
Box<dyn Animal>
可以按下面的顺序理解:
Animal
一个 trait 接口
dyn Animal
某个实现了 Animal 的未知具体类型
Box<dyn Animal>
在堆上拥有这个未知具体类型的对象
调用方法
运行时通过虚表找到真正实现
核心概念可以浓缩成一句话:
Box<dyn Trait>是一个拥有堆对象所有权的 trait 对象,用于在运行时保存和调用不同的 trait 实现,从而实现动态多态。
最典型的使用形式是:
let objects: Vec<Box<dyn Trait>> = vec![
Box::new(TypeA),
Box::new(TypeB),
Box::new(TypeC),
];
其中 TypeA、TypeB、TypeC 可以完全不同,只要它们都实现了同一个 Trait。