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

kFY*_*tek 40 oop language-design liskov-substitution-principle rust

鉴于此代码:

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) {}
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,我不能投&Derived&Base:

fn example(v: &Derived) {
    v as &Base;
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

这是为什么?该Derived虚函数表必须引用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
}
Run Code Online (Sandbox Code Playgroud)

所有Rust vtable都包含指向第一个字段中的析构函数,大小和对齐的指针,子引用vtable在引用supertrait方法时不会复制它们,也不会使用对supertrait vtables的间接引用.他们只是逐字地拥有方法指针的副本而没有别的.

鉴于这种设计,很容易理解为什么这不起作用.需要在运行时构建一个新的vtable,它可能存在于堆栈中,这并不是一个优雅(或最佳)的解决方案.

当然,有一些解决方法,比如在界面中添加显式的upcast方法,但这需要相当多的样板(或宏观狂热)才能正常工作.

现在,问题是 - 为什么不以某种方式实现能够实现特征对象向上转换?比如,在subtrait的vtable中添加指向supertrait的vtable的指针.目前,Rust的动态调度似乎不满足Liskov替换原则,这是面向对象设计的一个非常基本的原则.

当然你可以使用静态调度,这在Rust中使用确实非常优雅,但它很容易导致代码膨胀,有时比计算性能更重要 - 就像在嵌入式系统上一样,Rust开发人员声称支持这样的用例语言.此外,在许多情况下,您可以成功使用一个非纯粹面向对象的模型,这似乎是Rust的功能设计所鼓励的.尽管如此,Rust支持许多有用的OO模式......那么为什么不使用LSP呢?

有谁知道这种设计的理由?

kFY*_*tek 42

实际上,我认为我有理由.我找到了一种优雅的方式来为任何需要它的特性添加向上转换支持,这样程序员就可以选择是否将额外的vtable条目添加到特征中,或者更愿意不添加,这是类似的权衡. C++的虚拟与非虚拟方法:优雅和模型正确性与性能.

代码可以实现如下:

trait Base: AsBase {
    // ...
}

trait AsBase {
    fn as_base(&self) -> &Base;
}

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}
Run Code Online (Sandbox Code Playgroud)

可以添加用于转换&mut指针的附加方法或者Box(添加T必须是'static类型的要求),但这是一般性的想法.对于每个派生类型,这允许安全且简单(尽管不是隐式)向上转换每个派生类型而不使用样板.

  • “可以添加额外的方法来转换 &amp;mut 指针或 Box”——您能否提供一个 Box 情况的示例?我不清楚在这种情况下如何在不使用不安全的情况下使其工作。 (2认同)

Mas*_*ara 20

截至2017年6月,这种"次级特征胁迫"(或"超级特质胁迫")的状态如下:

  • 一个公认的RFC #0401提到这是强制的一部分.所以这种转换应该是隐含的.

    coerce_inner(T)= U其中T是一个子特征U;

  • 但是,这尚未实施.还有一个相应的问题#18600.

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

  • 基本上,问题是如何为超级特征推导vtable.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说他们准备了一份看起来井井有条的RFC草案,但在那之后它们看起来就像消失了(2016年11月).


oli*_*obk 16

当我开始使用Rust时,我遇到了同样的墙.现在,当我考虑特征时,我的想法与想到课程时的想法不同.

trait X: Y {}意味着当您实现特征X的结构S需要实现特征YS.

当然这意味着&X知道它也是一个&Y,因此提供了适当的功能.如果您需要首先遍历指向Yvtable的指针,则需要一些运行时工作(更多指针解引用).

然后,当前的设计+其他vtable的附加指针可能不会受到太大影响,并且可以实现简单的转换.那么也许我们需要两者?这是在internals.rust-lang.org上讨论的内容


Cha*_*man 5

Trait 对象升级在每晚 ( #118133 )上稳定下来,计划在 Rust 1.76.0(2024 年 2 月 8 日)上使用。问题中的代码几乎按原样工作,除了需要添加dyn到特征对象之外。

编辑:由于稳定性问题,稳定性已恢复。