不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-12 10:53:42  修改时间:2026-07-20 17:00:22  分类:01.Rust编程  编辑

目录

Box<dyn Trait> 是 Rust 中非常常见的“堆对象 + 接口类型 + 运行时多态”写法。

例如:

Box<dyn Animal>

可以理解为:

在堆上保存一个实现了 Animal trait 的具体对象,但外部不关心它究竟是 DogCat 还是其他类型,只通过 Animal 提供的方法操作它。

这有点类似 Java:

Animal animal = new Dog();

但 Rust 会明确表达:

  • Box:拥有对象,并将对象放到堆上;
  • dyn Animal:这个对象的具体类型在编译期不确定,只知道它实现了 Animal
  • 调用方法时通过运行时动态分发找到正确的方法。

一、先拆开看:Boxdyn Trait

1. Box<T>

Box<T> 是 Rust 的智能指针,用于:

  1. 把数据放到堆上;
  2. 拥有这块数据;
  3. 离开作用域后自动释放。
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

表示:

某个实现了 Animal trait 的对象,但不知道它具体是哪一种类型。

注意,不能直接这样写:

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,
    ];
}

因为 DogCat 是不同类型。

但是它们都实现了同一个 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,
}

DogBird 的大小不同。

假设:

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 Traitdyn Trait 不一样

很多初学者会把它们混在一起。

impl Trait

fn create_animal() -> impl Animal {
    Dog
}

意思是:

返回某一个确定的、实现了 Animal 的具体类型,只是不告诉调用者具体类型名称。

这个具体类型在编译时是确定的。

不能一个分支返回 Dog,另一个分支返回 Cat

fn create_animal(flag: bool) -> impl Animal {
    if flag {
        Dog
    } else {
        Cat
    }
}

除非 DogCat 实际上是同一个类型,否则编译失败。

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>

但是 SendSync 这类自动 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>

适合的场景包括:

  1. 一个集合中需要保存多种不同类型:
Vec<Box<dyn Plugin>>
  1. 函数可能返回不同的实现类型:
fn create_storage() -> Box<dyn Storage>
  1. 运行时根据配置选择不同策略:
Box<dyn Payment>
Box<dyn Logger>
Box<dyn Cache>
  1. 实现插件系统:
Vec<Box<dyn Plugin>>
  1. 保存不同类型的闭包:
Vec<Box<dyn Fn()>>
  1. 统一不同错误:
Box<dyn Error>
  1. 对外隐藏具体实现:
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),
];

其中 TypeATypeBTypeC 可以完全不同,只要它们都实现了同一个 Trait