之前的文章:
本章主要介绍 rust 的 boxed trait 相关机制,以及与运行时多态相关的语言特性。
Boxed Trait
在 rust 中,数组和 Vec 只能存放同一数据类型的数据。试图存放多个不同类型的数据会导致编译失败,例如:
struct Fruit {name: String,durability_days: u32,price: u32,}struct Book {name: String,author: String,price: u32,}fn main() {let apple = Fruit {name: format!("Apple"),durability_days: 30,price: 5,};let geometry = Book {name: format!("Geometry"),author: format!("Euclid"),price: 60,};let cart = vec![apple, geometry];// 编译失败!// apple 和 geometry 并不是同一个类型}
在上例中,你不能将 Fruit 和 Book 直接放在 cart 中,因为它们是不同的数据类型。要解决这个问题,可以定义一个 Product trait ,让 Fruit 和 Book 都实现这个 trait ,然后就可以用下例中的方式将它们都放在 cart 中:
struct Fruit {name: String,durability_days: u32,price: u32,}struct Book {name: String,author: String,price: u32,}trait Product {fn price(&self) -> u32;}// 为 Fruit 实现 Productimpl Product for Fruit {fn price(&self) -> u32 {self.price}}// 为 Book 实现 Productimpl Product for Book {fn price(&self) -> u32 {self.price}}fn main() {let apple = Fruit {name: format!("Apple"),durability_days: 30,price: 5,};let geometry = Book {name: format!("Geometry"),author: format!("Euclid"),price: 60,};// cart 中的每一项是 Box<dyn Product> 类型let cart: Vec<Box<dyn Product>> = vec![// 将 Fruit 放入 Box 中// Box<Fruit> 可以用作 Box<dyn Product>Box::new(apple),// Box<Book> 也可以用作 Box<dyn Product>Box::new(geometry),];let mut total_price = 0;for item in &cart {// 每个 item 都是 &Box<dyn Product>// 可以调用 trait Product 中的函数total_price += item.price();}println!("{}", total_price); // 输出 65}
这种方式称为 boxed trait ,即 Box 内的数据类型可以被转换为该数据类型的一个 trait 。这样,可以将这个 trait 的多个具体数据类型“泛化”成一个“泛用的”数据类型。
不过,有些 trait 并不能这样使用,典型的例子是 trait 中包含的函数参数或返回值中有 Self :
trait Product {fn price(&self) -> u32;fn my_trait_fn(&self) -> Self;}// 不能作为 Box<dyn Product> 使用!
这种情况下,需要加上一个限制 Self: Sized :
trait Product {fn price(&self) -> u32;// 下面这个函数不对 Box<dyn Product> 使用fn my_trait_fn(&self) -> Self where Self: Sized;}
这样就可以作为 boxed trait 使用了。(不过,受限的函数本身只能在 boxed trait 以外的情况下使用。)
Downcast
通常 boxed trait 只能调用 trait 中定义的函数。有些情况下,希望调用到具体数据类型的关联函数,此时可以将 boxed trait “特化”为具体数据类型。这个过程称为 downcast 。
第三方 crate downcast-rs 可以辅助实现对 boxed trait 的 downcast 。首先在 Cargo.toml 中引入:
[dependencies]downcast-rs = "1.2"
具体用法示例:
use downcast_rs::{Downcast, impl_downcast};struct Fruit {name: String,durability_days: u32,price: u32,}struct Book {name: String,author: String,price: u32,}// 声明为支持 Downcast 的 traittrait Product: Downcast {fn price(&self) -> u32;}// 为 Product 添加 downcast 支持impl_downcast!(Product);impl Product for Fruit {fn price(&self) -> u32 {self.price}}impl Product for Book {fn price(&self) -> u32 {self.price}}fn main() {let apple = Fruit {name: format!("Apple"),durability_days: 30,price: 5,};let geometry = Book {name: format!("Geometry"),author: format!("Euclid"),price: 60,};let cart: Vec<Box<dyn Product>> = vec![Box::new(apple),Box::new(geometry),];for item in &cart {// 尝试 downcast 为 &Fruit 类型if let Some(fruit) = item.downcast_ref::<Fruit>() {// 如果是 Fruit 类型,执行这里println!("{}", fruit.durability_days); 输出 30}}}
Boxed Trait 和 enum 的选用
需要注意的是, boxed trait 是有开销的。具体来说, boxed trait 附带有一个“具体是哪个数据类型”的信息,并在运行期间,根据具体的数据类型来选用 trait 函数的对应实现。
相对而言,由于 enum 是零开销的,实践中常用 enum 来代替 boxed trait ,例如:
struct Fruit {name: String,durability_days: u32,price: u32,}struct Book {name: String,author: String,price: u32,}enum Product {Fruit(Fruit),Book(Book),}fn main() {let apple = Fruit {name: format!("Apple"),durability_days: 30,price: 5,};let geometry = Book {name: format!("Geometry"),author: format!("Euclid"),price: 60,};let cart: Vec<Product> = vec![Product::Fruit(apple),Product::Book(geometry),];let mut total_price = 0;for item in &cart {total_price += match item {Product::Book(b) => b.price,Product::Fruit(f) => f.price,};}println!("{}", total_price); // 输出 65}
这种使用 enum 的方式具有更好的性能。不过它仅适用于 Product 的种类可以逐个列举的情况(上例中只有 Fruit 和 Book 两类),如果 Product 的种类很多,就很难用甚至不能用这种方法了。
此外,这种 enum 的方式会往往需要大量的 match 来逐个处理每个分支,使得代码比较难读。对于这个问题,第三方 crate enum_dispatch 提供了一种很好的解决方式,让代码看起来更像 boxed trait ,但具有 enum 的性能。首先在 Cargo.toml 中引入:
[dependencies]enum_dispatch = "0.3"
具体用法示例:
use enum_dispatch::enum_dispatch;struct Fruit {name: String,durability_days: u32,price: u32,}struct Book {name: String,author: String,price: u32,}// 列举 Product 所有可能的具体类型#[enum_dispatch]enum Product {Fruit,Book,}// 定义 Product 所需的 trait#[enum_dispatch(Product)]trait ProductTrait {fn price(&self) -> u32;}impl ProductTrait for Fruit {fn price(&self) -> u32 {self.price}}impl ProductTrait for Book {fn price(&self) -> u32 {self.price}}fn main() {let apple = Fruit {name: format!("Apple"),durability_days: 30,price: 5,};let geometry = Book {name: format!("Geometry"),author: format!("Euclid"),price: 60,};let cart: Vec<Product> = vec![apple.into(),geometry.into(),];let mut total_price = 0;for item in &cart {total_price += item.price();}println!("{}", total_price); // 输出 65}
这种方法依然只适用于 Product 具体类型可以被逐个列举的情况。
boxed trait 还有另一些更重要、不可替代的使用场景,这将在之后的文章中介绍。




