为什么Rust不支持trait对象向上转换?

7uzetpgm  于 2022-11-12  发布在  其他
关注(0)|答案(5)|浏览(256)

给定以下代码:

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不支持呢?
有人知道这样设计的理由吗?

guz6ccqo

guz6ccqo1#

实际上,我想我已经找到了原因。我找到了一种优雅的方法,可以为任何需要的trait添加上转换支持,这样程序员就可以选择是否添加额外的vtable条目到trait中,这与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类型的要求),但这是一个通用的想法。这允许安全和简单(尽管不是隐式的)的每个派生类型的上转换,而不需要每个派生类型的样板。

m1m5dgzv

m1m5dgzv2#

截至2017年6月,这种“次特质强制”(或“超特质强制”)的状态如下:

  • 一个公认的RFC #0401将此作为强制的一部分。因此,此转换应隐式完成。

强制_内部(T)= U其中TU的子特性;

  • 但是,这还没有实现。有一个相应的问题#18600

还有一个重复的问题#5665。那里的评论解释了是什么阻止了这一点的实现。

  • 基本上,问题是如何导出超级特性的vtables。当前vtables的布局如下(在x86-64的情况下):
+-----+-------------------------------+
| 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做一些调整。

  • 当然,有很多方法可以缓解这个问题,但是有很多方法都有不同的优缺点!一种方法在有钻石继承时对vtable的大小有好处。另一种方法应该更快。

@typelist说他们准备了a draft RFC,看起来组织得很好,但在那之后(2016年11月),它们看起来就像消失了一样。

dddzy1tm

dddzy1tm3#

当我开始研究Rust的时候,我也遇到了同样的问题。现在,当我想到特质的时候,我脑海中的形象与我想到类的时候不同。
trait X: Y {}表示当您为结构体S实现特征X时,您还需要S实现特征Y
当然,这意味着&X知道它也是&Y,因此提供了相应的函数。如果你需要先遍历指向Y的vtable的指针,这将需要一些运行时工作(更多的指针解引用)。
但是,当前的设计+指向其他vtable的额外指针可能不会造成太大的伤害,并且允许实现简单的强制转换。所以,也许我们两者都需要?这将在internals.rust-lang.org上讨论

k7fdbhmy

k7fdbhmy4#

这个特性是如此的受欢迎,以至于既有一个将它添加到语言中的跟踪问题,也有一个专门的计划库供实现它的人使用。
跟踪问题:https://github.com/rust-lang/rust/issues/65991
方案存储库:https://github.com/rust-lang/dyn-upcasting-coercion-initiative

9vw9lbht

9vw9lbht5#

现在这是在稳定的rust上工作的,你可以向上转换到基本特征,也可以直接从派生特征对象调用基本特征函数

trait Base {
   fn a(&self) {
     println!("a from base");
   }
}

trait Derived: Base {
   fn e(&self) {
     println!("e from derived");
   }
}

fn call_derived(d: &impl Derived) {
   d.e();
   d.a();
   call_base(d);
}

fn call_base(b: &impl Base) {
   b.a();
}

struct S;
impl Base for S {}
impl Derived for S {}

fn main() {
   let s = S;
   call_derived(&s);
}

playground link

相关问题