在某些情况下,当编写软件时,开发人员会遇到异构集合的需求——也就是说,可以存储不同类型对象的集合。在Rust中,开发人员可以通过不同的方式实现这一目标,并进行不同的权衡。
Rust枚举是实现这一目标的好方法,如果要存储的对象的所有实现在开发时都是已知的,那么开发人员可以创建一个枚举来封装每种可能的类型,然后为这些枚举创建一个集合。
然后,为了访问内部类的方法和属性,可以使用匹配表达式来检索内部对象。
enum ComponentType {
FirstComponent(MyFirstComponent),
SecondComponent(MySecondComponent),
}
struct MyFirstComponent {
}
impl MyFirstComponent {
fn do_first_component_thing(&self) {
println!("First Component");
}
}
struct MySecondComponent {
}
impl MySecondComponent {
fn do_second_component_thing(&self) {
println!("Second Component");
}
}
fn main() {
// 创建一个枚举集合
let mut components: Vec<ComponentType> = Vec::new();
// 将枚举添加到集合中,封装目标类型
components.push(ComponentType::FirstComponent(MyFirstComponent {}));
components.push(ComponentType::SecondComponent(MySecondComponent {}));
// 使用匹配表达式从枚举中检索对象,并访问方法和属性
if let ComponentType::FirstComponent(component) = &components[0] {
component.do_first_component_thing();
}
}
这种方法的一个缺点是,在编写代码时需要确切知道这些被存储的组件类型。
另一种方法是使用特征作为替代解决方案,其中:
trait Component {
fn do_component_thing(&self);
}
struct MyFirstComponent {}
impl Component for MyFirstComponent {
fn do_component_thing(&self) {
println!("First Component");
}
}
struct MySecondComponent {}
impl Component for MySecondComponent {
fn do_component_thing(&self) {
println!("Second Component");
}
}
fn main() {
let mut components: Vec<Box<dyn Component>> = Vec::new();
components.push(Box::new(MyFirstComponent { }));
components.push(Box::new(MySecondComponent { }));
components[0].do_component_thing();
components[1].do_component_thing();
}
当只需要访问所有特征中的公共方法时,这种方法工作得很好。这种方法的一个缺点是元素总是在堆上分配,另一个缺点是只能访问公共方法。
Rust文档将Any类型描述为一个模拟动态类型的Trait,它提供了一个向下转换方法,允许将一个类型转换为不同的类型。
use std::any::Any;
struct MyFirstComponent {
}
impl MyFirstComponent {
fn do_first_component_thing(&self) {
println!("First Component");
}
}
struct MySecondComponent {
}
impl MySecondComponent {
fn do_second_component_thing(&self) {
println!("Second Component");
}
}
fn main() {
let mut components: Vec<Box<dyn Any>> = Vec::new();
components.push(Box::new(MyFirstComponent {}));
components.push(Box::new(MySecondComponent {}));
if let Some(component) =
components[0].downcast_ref::<MyFirstComponent>() {
component.do_first_component_thing();
}
if let Some(component) =
components[1].downcast_ref::<MySecondComponent>() {
component.do_second_component_thing();
}
}
虽然这仍然会始终在堆上分配对象,但现在可以在数据结构中使用不同的组件类型,将它们转换为原始类型并访问组件特定的属性和方法。
不过,有一个小问题——没有限制哪些类型可以添加到结构中。
Any可以与Trait一起使用来为对象创建边界。诀窍是在trait中添加一个方法,将对象转换为Any,然后将其向下转换为其他对象。然后,每个结构体都必须实现trait和转换方法:
use std::any::Any;
trait Component {
fn as_any(&self) -> &dyn Any;
}
struct MyFirstComponent {
}
impl MyFirstComponent {
fn do_first_component_thing(&self) {
println!("First Component");
}
}
impl Component for MyFirstComponent {
fn as_any(&self) -> &dyn Any {
self
}
}
struct MySecondComponent {
}
impl MySecondComponent {
fn do_second_component_thing(&self) {
println!("Second Component");
}
}
impl Component for MySecondComponent {
fn as_any(&self) -> &dyn Any {
self
}
}
fn main() {
let mut components: Vec<Box<dyn Component>> = Vec::new();
components.push(Box::new(MyFirstComponent {}));
components.push(Box::new(MySecondComponent {}));
if let Some(component) =
components[0].as_any().downcast_ref::<MyFirstComponent>() {
component.do_first_component_thing();
}
if let Some(component) =
components[1].as_any().downcast_ref::<MySecondComponent>() {
component.do_second_component_thing();
}
}
虽然这仍然会在堆上分配对象,但对象特定的方法和属性可以与向下转换一起使用,并且集合绑定到实现该特征的对象。一个很大的缺点是必须为每个对象实现trait,这恰恰是样板代码。
使用过程性宏来实现减少样板代码的解决方案:
// 派生宏需要驻留在自己的crate中
#[proc_macro_derive(Component)]
pub fn component_macro_derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl Component for #name {
fn as_any(&self) -> &dyn Any {
self
}
}
};
gen.into()
}
// Component仍然存在于项目文件中.
#[derive(Component)]
struct MyFirstComponent {
}
impl MyFirstComponent {
fn do_first_component_thing(&self) {
println!("First Component");
}
}
在Rust中有不同的方法来实现异构集合,虽然使用枚举似乎被认为是最常用的方法,但并不适合所有情况。在某些特定情况下,可以使用Trait和Any。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8