在异构 Rust 集合中使用 Any trait 对象有什么问题?
What are the problems of using Any trait objects in heterogeneous Rust collections?
在 Clojure 等动态语言中,很容易表达不同类型的集合:
{:key1 "foo", :key2 [34 "bar" 4.5], "key3" {:key4 "foobar"}}
在 Rust 中,实现此类集合的首选方法是 using trait objects or enums。 Any
特征对象的使用似乎是最灵活的方法(如果没有固定数量的已知类型替代),因为它允许向下转换为实际对象类型:
let mut vector: Vec<Box<Any>> = Vec::new();
vector.push(Box::new("I’m"));
vector.push(Box::new(4 as u32));
console!(log, vector[0].downcast_ref::<&str>());
console!(log, vector[1].downcast_ref::<u32>());
这种方法似乎不被鼓励。它的缺点是什么?
What are its disadvantages?
主要的缺点是,当您从 &Any
的集合中访问一个值时,您唯一能做的就是将它向下转换为特定的已知类型。如果有一个你不知道的类型,那么那个类型的值是完全不透明的:你可以对它们做 nothing 除了计算有多少。如果您知道具体类型,那么您可以向下转型,但您需要尝试每一种可能的类型。
这是一个例子:
let a = 1u32;
let b = 2.0f32;
let c = "hello";
let v: Vec<&dyn Any> = vec![&a, &b, &c];
match v[0].downcast_ref::<u32>() {
Some(x) => println!("u32: {:?}", x),
None => println!("Not a u32!"),
}
请注意,我必须明确向下转换为 u32
。使用这种方法将涉及每个可能的具体类型的逻辑分支,如果我忘记了一个案例,则不会出现编译器警告。
Trait 对象更通用,因为您不需要知道具体类型就可以使用这些值 - 只要您坚持只使用 trait 的方法。
例如:
let v: Vec<&dyn Debug> = vec![&a, &b, &c];
println!("u32: {:?}", v[0]); // 1
println!("u32: {:?}", v[1]); // 2.0
println!("u32: {:?}", v[2]); // "hello"
我能够在不知道具体类型的情况下使用所有值,因为我只使用了它们实现 Debug
.
的事实
与在同类集合中使用具体类型相比,这两种方法都有一个缺点:一切都隐藏在指针后面。访问数据总是间接的,并且数据最终可能会散布在内存中,从而大大降低访问效率并且编译器更难优化。
使用枚举使集合同质化如下所示:
enum Item<'a> {
U32(u32),
F32(f32),
Str(&'a str),
}
let v: Vec<Item> = vec![Item::U32(a), Item::F32(b), Item::Str(c)];
match v[0] {
Item::U32(x) => println!("u32: {:?}", x),
Item::F32(x) => println!("u32: {:?}", x),
Item::Str(x) => println!("u32: {:?}", x),
}
在这里,我仍然必须知道所有的类型,但至少如果我错过了一个,编译器会有一个警告。另请注意,枚举可以拥有自己的值,因此(除了本例中的 &str
之外)数据可以紧密地打包在内存中,从而加快访问速度。
总而言之,Any
很少是异构集合的正确答案,但特征对象和枚举都有自己的权衡。
在 Clojure 等动态语言中,很容易表达不同类型的集合:
{:key1 "foo", :key2 [34 "bar" 4.5], "key3" {:key4 "foobar"}}
在 Rust 中,实现此类集合的首选方法是 using trait objects or enums。 Any
特征对象的使用似乎是最灵活的方法(如果没有固定数量的已知类型替代),因为它允许向下转换为实际对象类型:
let mut vector: Vec<Box<Any>> = Vec::new();
vector.push(Box::new("I’m"));
vector.push(Box::new(4 as u32));
console!(log, vector[0].downcast_ref::<&str>());
console!(log, vector[1].downcast_ref::<u32>());
这种方法似乎不被鼓励。它的缺点是什么?
What are its disadvantages?
主要的缺点是,当您从 &Any
的集合中访问一个值时,您唯一能做的就是将它向下转换为特定的已知类型。如果有一个你不知道的类型,那么那个类型的值是完全不透明的:你可以对它们做 nothing 除了计算有多少。如果您知道具体类型,那么您可以向下转型,但您需要尝试每一种可能的类型。
这是一个例子:
let a = 1u32;
let b = 2.0f32;
let c = "hello";
let v: Vec<&dyn Any> = vec![&a, &b, &c];
match v[0].downcast_ref::<u32>() {
Some(x) => println!("u32: {:?}", x),
None => println!("Not a u32!"),
}
请注意,我必须明确向下转换为 u32
。使用这种方法将涉及每个可能的具体类型的逻辑分支,如果我忘记了一个案例,则不会出现编译器警告。
Trait 对象更通用,因为您不需要知道具体类型就可以使用这些值 - 只要您坚持只使用 trait 的方法。
例如:
let v: Vec<&dyn Debug> = vec![&a, &b, &c];
println!("u32: {:?}", v[0]); // 1
println!("u32: {:?}", v[1]); // 2.0
println!("u32: {:?}", v[2]); // "hello"
我能够在不知道具体类型的情况下使用所有值,因为我只使用了它们实现 Debug
.
与在同类集合中使用具体类型相比,这两种方法都有一个缺点:一切都隐藏在指针后面。访问数据总是间接的,并且数据最终可能会散布在内存中,从而大大降低访问效率并且编译器更难优化。
使用枚举使集合同质化如下所示:
enum Item<'a> {
U32(u32),
F32(f32),
Str(&'a str),
}
let v: Vec<Item> = vec![Item::U32(a), Item::F32(b), Item::Str(c)];
match v[0] {
Item::U32(x) => println!("u32: {:?}", x),
Item::F32(x) => println!("u32: {:?}", x),
Item::Str(x) => println!("u32: {:?}", x),
}
在这里,我仍然必须知道所有的类型,但至少如果我错过了一个,编译器会有一个警告。另请注意,枚举可以拥有自己的值,因此(除了本例中的 &str
之外)数据可以紧密地打包在内存中,从而加快访问速度。
总而言之,Any
很少是异构集合的正确答案,但特征对象和枚举都有自己的权衡。