泛型、trait 与闭包 intermediate 25 分钟 更新 2026-06-15

trait 对象与 dyn

用 Box<dyn Trait> 在运行时存放不同类型。

trait 对象与 dyn

你会学到什么

  • Box<dyn Trait> 让一个集合存放多种实现了同一 trait 的类型。
  • trait 对象用动态分发,运行时根据实际类型决定调用哪份方法。
  • 静态分发(泛型)和动态分发(dyn)各自的取舍。

最小示例

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];

运行代码

cd examples
cargo run -p rt_18_trait_objects
cargo test -p rt_18_trait_objects

代码讲解

泛型 Vec<T> 只能装同一种类型;当你需要把 CircleSquare 放进同一个 Vec,就用 trait 对象:

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|shape| shape.area()).sum()
}

dyn Shape 在运行时通过虚表(vtable)找到正确的 area 实现,这叫动态分发。代价是一次指针跳转和无法内联;泛型则在编译期确定,速度更快但每种类型生成一份代码。

常见错误

把 trait 对象直接放在栈上(大小未知):

let shape: dyn Shape = Circle { radius: 1.0 }; // ❌ size not known

trait 对象必须在指针后面:Box<dyn Shape>&dyn Shape 等。

练习

  • Shape 再加一个 Triangle 实现,放进同一个 Vec
  • 写一个函数返回面积最大的形状的名字。

小结

dyn Trait 提供运行时多态,适合“异构集合”和插件式设计;性能敏感且类型已知时优先用泛型。

下一步

闭包是 Rust 里随处可见的“匿名函数 + 捕获环境”。下一章学习闭包。

完整示例代码

下面是 examples/18_trait_objects/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/18_trait_objects/src/main.rs
//! trait 对象:用 dyn 在运行时存放不同类型。

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    fn name(&self) -> &str {
        "circle"
    }
}

struct Square {
    side: f64,
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
    fn name(&self) -> &str {
        "square"
    }
}

/// 一个 Vec 里放多种实现了 Shape 的类型。
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|shape| shape.area()).sum()
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    for shape in &shapes {
        println!("{} => {:.2}", shape.name(), shape.area());
    }
    println!("total = {:.2}", total_area(&shapes));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sums_mixed_shapes() {
        let shapes: Vec<Box<dyn Shape>> = vec![
            Box::new(Square { side: 2.0 }),
            Box::new(Square { side: 3.0 }),
        ];
        assert_eq!(total_area(&shapes), 13.0);
    }

    #[test]
    fn reports_name() {
        let circle = Circle { radius: 1.0 };
        assert_eq!(circle.name(), "circle");
    }
}
examples/18_trait_objects/Cargo.toml
[package]
name = "rt_18_trait_objects"
version.workspace = true
edition.workspace = true
publish.workspace = true