鉴于此代码:
trait Base {
fn a(&self);
fn b(&self);
fn c(&self);
fn d(&self);
}
trait Derived : Base {
fn e(&self);
fn f(&self);
fn g(&self);
}
struct S;
impl Derived for S {
fn e(&self) {}
fn f(&self) {}
fn g(&self) {}
}
impl Base for S {
fn a(&self) {}
fn b(&self) {}
fn c(&self) {}
fn d(&self) {}
}
很遗憾,我无法将&Derived
投射到&Base
:
fn example(v: &Derived) {
v as &Base;
}
error[E0605]: non-primitive cast: `&Derived` as `&Base`
--> src/main.rs:30:5
|
30 | v as &Base;
| ^^^^^^^^^^
|
= note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait
为什么? Derived
vtable必须以某种方式引用Base
方法。
检查LLVM IR会显示以下内容:
@vtable4 = internal unnamed_addr constant {
void (i8*)*,
i64,
i64,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*
} {
void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
i64 0,
i64 1,
void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}
@vtable26 = internal unnamed_addr constant {
void (i8*)*,
i64,
i64,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*,
void (%struct.S*)*
} {
void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
i64 0,
i64 1,
void (%struct.S*)* @_ZN9S.Derived1e20h9992ddd0854253d1WaaE,
void (%struct.S*)* @_ZN9S.Derived1f20h849d0c78b0615f092aaE,
void (%struct.S*)* @_ZN9S.Derived1g20hae95d0f1a38ed23b8aaE,
void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}
所有Rust vtable包含指向第一个字段中的析构函数,大小和对齐的指针,并且子引用vtable在引用supertrait方法时不会复制它们,也不会使用对supertrait vtables的间接引用。他们只是逐字地拥有方法指针的副本而没有别的。
鉴于这种设计,很容易理解为什么这不起作用。需要在运行时构建一个新的vtable,它可能存在于堆栈中,而这并不是一个优雅(或最佳)的解决方案。
当然,有一些解决方法,比如在界面中添加显式的upcast方法,但这需要相当多的样板(或宏观狂热)才能正常工作。
现在,问题是 - 为什么不能以某种方式实现特征对象向上转换?比如,在subtrait的vtable中添加指向supertrait的vtable的指针。目前,Rust的动态调度似乎并不满足Liskov substitution principle,这是面向对象设计的一个非常基本的原则。
当然你可以使用静态调度,这在Rust中使用确实非常优雅,但它很容易导致代码膨胀,有时比计算性能更重要 - 就像嵌入式系统一样,Rust开发人员声称支持这种使用语言的案例。此外,在许多情况下,您可以成功使用一个非纯粹面向对象的模型,这似乎是Rust的功能设计所鼓励的。尽管如此,Rust支持许多有用的OO模式......那么为什么不用LSP?
有谁知道这种设计的理由?
答案 0 :(得分:38)
实际上,我认为我有理由。我找到了一种优雅的方式来为任何需要它的特性添加向上转换支持,这样程序员就可以选择是否将额外的vtable条目添加到特征中,或者更愿意不添加,这是类似的权衡。 C ++的虚拟与非虚拟方法:优雅和模型正确性与性能。
代码可以按如下方式实现:
trait Base: AsBase {
// ...
}
trait AsBase {
fn as_base(&self) -> &Base;
}
impl<T: Base> AsBase for T {
fn as_base(&self) -> &Base {
self
}
}
可以添加其他方法来投射&mut
指针或Box
(这会增加T
必须是'static
类型的要求),但这是一个大概的概念。这允许每个派生类型的安全和简单(尽管不是隐式)向上转换,而不是每个派生类型的样板。
答案 1 :(得分:18)
截至2017年6月,这个&#34;子特征强制的状态&#34; (或&#34;超特质强制&#34;)如下:
还有一个重复的问题#5665。那里的评论解释了阻止其实施的原因。
+-----+-------------------------------+ | 0- 7|pointer to "drop glue" function| +-----+-------------------------------+ | 8-15|size of the data | +-----+-------------------------------+ |16-23|alignment of the data | +-----+-------------------------------+ |24- |methods of Self and supertraits| +-----+-------------------------------+它不包含超级特征的vtable作为子序列。我们至少要对vtable进行一些调整。
有@typelist说他们准备a draft RFC看起来组织得很好,但在那之后它们看起来就像消失了(2016年11月)。
答案 2 :(得分:16)
当我开始使用Rust时,我遇到了同样的墙。 现在,当我考虑特征时,我的想法与想到课程时的想法不同。
trait X: Y {}
表示当您为结构X
实施特征S
时,您还需要为Y
实施特征S
。
当然这意味着&X
知道它也是&Y
,因此提供了适当的功能。
如果你需要首先遍历指向Y
的vtable的指针,那么需要一些运行时工作(更多的指针解引用)。
然后,当前的设计+其他vtable的附加指针可能不会造成太大的伤害,并且可以实现轻松的转换。那么也许我们需要两者?这是在internals.rust-lang.org
上讨论的内容