给定以下代码:
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
:
第一个
为什么?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 vtables都包含一个指向析构函数的指针,在第一个字段中包含大小和对齐方式,并且subtrait vtables在引用supertrait方法时不会复制它们,也不会间接引用supertrait vtables。它们只会原封不动地复制方法指针,没有其他内容。
在这种设计下,很容易理解为什么这样做不起作用。在运行时需要构造一个新的vtable,它可能驻留在堆栈上,这并不是一个优雅(或最优)的解决方案。
当然,也有一些变通办法,比如在接口中添加显式的upcast方法,但这需要相当多的样板(或宏疯狂)才能正常工作。
现在,问题是--为什么它没有以某种方式实现,以支持trait对象的向上转换?比如,在subtrait的vtable中添加一个指向supertrait的vtable的指针。目前,Rust的动态调度似乎还不满足Liskov substitution principle,这是面向对象设计的一个非常基本的原则。
当然,你可以使用静态调度,这在Rust中使用起来确实非常优雅,但是它很容易导致代码膨胀,有时候代码膨胀比计算性能更重要--比如在嵌入式系统上,Rust开发人员声称支持这种语言的用例。这似乎受到了Rust的功能设计的鼓励。尽管如此,Rust支持许多有用的OO模式......那么为什么LSP不支持呢?
有人知道这样设计的理由吗?
5条答案
按热度按时间guz6ccqo1#
实际上,我想我已经找到了原因。我找到了一种优雅的方法,可以为任何需要的trait添加上转换支持,这样程序员就可以选择是否添加额外的vtable条目到trait中,这与C++的虚拟方法和非虚拟方法的权衡类似:优雅性和模型正确性与性能。
该代码可按如下方式实现:
可以添加额外的方法来转换
&mut
指针或Box
(这增加了T
必须是'static
类型的要求),但这是一个通用的想法。这允许安全和简单(尽管不是隐式的)的每个派生类型的上转换,而不需要每个派生类型的样板。m1m5dgzv2#
截至2017年6月,这种“次特质强制”(或“超特质强制”)的状态如下:
强制_内部(
T
)=U
其中T
是U
的子特性;还有一个重复的问题#5665。那里的评论解释了是什么阻止了这一点的实现。
它不包含一个作为子序列的超级特性的vtable。我们至少要对vtable做一些调整。
@typelist说他们准备了a draft RFC,看起来组织得很好,但在那之后(2016年11月),它们看起来就像消失了一样。
dddzy1tm3#
当我开始研究Rust的时候,我也遇到了同样的问题。现在,当我想到特质的时候,我脑海中的形象与我想到类的时候不同。
trait X: Y {}
表示当您为结构体S
实现特征X
时,您还需要为S
实现特征Y
。当然,这意味着
&X
知道它也是&Y
,因此提供了相应的函数。如果你需要先遍历指向Y
的vtable的指针,这将需要一些运行时工作(更多的指针解引用)。但是,当前的设计+指向其他vtable的额外指针可能不会造成太大的伤害,并且允许实现简单的强制转换。所以,也许我们两者都需要?这将在internals.rust-lang.org上讨论
k7fdbhmy4#
这个特性是如此的受欢迎,以至于既有一个将它添加到语言中的跟踪问题,也有一个专门的计划库供实现它的人使用。
跟踪问题:https://github.com/rust-lang/rust/issues/65991
方案存储库:https://github.com/rust-lang/dyn-upcasting-coercion-initiative
9vw9lbht5#
现在这是在稳定的rust上工作的,你可以向上转换到基本特征,也可以直接从派生特征对象调用基本特征函数
playground link