我正在用 Rust 编写一个玩具数据库实现,但我无法绕过这个对象安全的特征要求。我基本上想要一种用 Rust 表示具有不同类型列的数据库表的方法。 这就是我当前现有代码的样子。
trait Primative {}
impl Primative for i32 {}
struct Table {
value: HashMap<String, Arc<dyn ColType>>,
}
trait ColType {
fn get_data(&self) -> &Vec<impl Primative>;
}
struct TableValue<T: Primative> {
data: Vec<T>
}
impl <T: Primative> ColType for TableValue<T> {
fn get_data(&self) -> &Vec<T> {
return &self.data;
}
}
这是我在数据存储方面取得的进展的简化版本。话虽如此,这段代码给出了警告
ColType
无法制作成对象。基本上我只想编译这个或一些类似的解决方案。要求是:
a)我需要 Primative 作为一个特征(在 i32 等上实现)。这是因为我不想使用枚举,因为我不确定如何确保每个值都是相同的变体以及开销。
b)我不想做任何
Vec<Box<impl Primative>>
的事情,因为我相信应该有办法不做那么糟糕的事情。 (当 T: Primative 工作的 Vec 时,为什么我要为每个项目创建一个新指针!)
c)解决方案实际上必须能够存储内容(通过向
HashMap<String, Arc<dyn ColType<dyn Primative>>>
添加通用参数,我得到了一些接近于使用 ColType<T: Primative>
的东西,但这最终导致了无法满足的特征边界。
在我看来,我应该能够返回一些类型为
Vec<impl ColType + Sized>
的堆分配 vec,但这可能只是我。
另外,对于任何拼写错误,我深表歉意。我在手机上写这篇文章是因为堆栈溢出网站无法在 librewolf 上运行。
如果这只是一个技能问题,请告诉我。
在特征方法中返回
&Vec<impl Primative>
会使特征失去对象安全性,因为编译器无法知道堆分配项的布局或对齐方式,因此无法保证运行时的内存安全。这也让我困惑了好几次,因为 dyn Trait
符号让我根据里氏替换原则进行思考,但它实际上是关于指针以及我们指向 CPU 的位置。 dyn Trait
类型是胖指针;它存储一个指向对象的指针(在本例中,是实现 Trait
的类型)和指向 vtable 的指针。后者保存有关类型的各种信息,例如在哪里可以找到某些方法的机器代码等。这非常酷,并且允许动态调度;在运行时,vtable 用于将 CPU 指向可以找到我们要调用的方法的正确位置。
另一方面,编译器需要知道
get_data
为 ColType
特征的所有实现者返回的确切类型,这对于 impl Primative 来说是不可能的。如果它不知道返回的确切类型,它就无法编译用于动态分派的胖指针,因为它不知道 vtable 指向哪里。
我可能会解释得很糟糕,所以我会推荐 Jon Gjengset 的书 Rust for Rustaceans,它详细介绍了这个主题(我上周刚读过这一章,这就是我回答的原因这个问题)。
确实,使用
Box
可以解决这个问题,但这会带来运行时性能损失。您说过您不想使用枚举,因为开销(?)和“我不确定如何确保每个值都是相同的变体”,但这不正是 Rust 的闪光点吗?我的意思是,模式匹配是最酷的事情之一(以我的愚见;))。
enum Primative {
Int(i32),
// Add more types here if needed, e.g., Float(f64)
}
struct Table {
value: HashMap<String, Arc<dyn ColType>>,
}
trait ColType {
fn get_data(&self) -> &Vec<Primative>;
}
struct TableValue {
data: Vec<Primative>,
}
impl ColType for TableValue {
fn get_data(&self) -> &Vec<Primative> {
&self.data
}
}
现在,编译器知道
ColType::get_data()
特征方法的返回值的确切类型,因为它们始终是 Primative
类型。唯一的缺点是内存使用:&Vec<Primative>
中的每个项目都与Primative
的最大可能变体一样大。