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

81

考虑下面的代码:

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类的虚表需要以某种方式引用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 引用超级特质方法时不会复制它们,也不会使用间接引用到超级特质 vtable。它们只是按原样拥有方法指针副本,没有其他内容。

考虑到这种设计,很容易理解为什么这不起作用。新的 vtable 需要在运行时构建,可能驻留在堆栈上,这并不是一种优雅(或最佳)的解决方案。

当然,有一些解决方法,比如添加显式上转型方法到接口中,但这需要相当多的样板文件(或宏狂潮)才能正常工作。

现在的问题是 - 为什么不能以某种方式实现,使特质对象向上转型成为可能?比如,在子特质的 vtable 中添加指向超级特质 vtable 的指针。目前,Rust 的动态分派似乎不符合 Liskov 替换原则,这是面向对象设计的一个非常基本的原则。

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

有没有人知道这种设计的原理?


20
附带说明:Rust不是面向对象的语言。Traits 不是接口,它们更像来自 Haskell 的类型类(type classes)。Rust 也没有子类型化,所以 LSP 对其有些不适用,因为其定义与子类型关系有关。 - Vladimir Matveev
13
不过,正如我所说的那样,Rust支持许多面向对象的抽象,并且trait允许继承,形成类似类型层次结构的东西。对于我来说,即使OO不是该语言的主要范例,支持LSP trait对象也似乎很自然。 - kFYatek
1
请确保给有用的答案点赞,并在解决了您的问题后将一个答案标记为已接受!如果没有可接受的答案,请考虑留下评论解释原因,或编辑您的问题以不同的方式表达。 - Shepmaster
1
这个问题在 Rust 的一个问题追踪页面中有记录:https://github.com/rust-lang/rust/issues/5665 (我看到你已经找到了,只是在这里提供一个链接。) - Jim Blandy
你是怎么得到这个LLVM IR的? - Guerlando OCs
5个回答

74
实际上,我认为我已经找到了原因。我找到了一种优雅的方法来为任何希望支持向上转型的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 类型),但这只是一个通用的想法。这允许对每个派生类型进行安全且简单(虽然不是隐式的)向上转换而无需为每个派生类型编写样板代码。


@Shepmaster 在 Rust 的今天,这应该是 AsRef<Base> 吗? - Bergi
1
@Bergi AsRef自Rust 1.0以来就可用,但我不确定你是否可以在这里使用它。一些快速尝试显示出各种错误。 - Shepmaster
2
可以为转换 &mut 指针或 Box 添加其他方法。你能提供一个 Box 的例子吗?如果不使用 unsafe,我不太清楚在这种情况下如何使其工作。 - Chris Suter
1
@kFYatek 我在我的代码库中采用了类似的方法,但是我在使用Box时遇到了一些困难。你已经成功地使用它了吗? - phimuemue
箱子 downcast-rs 实现了上述模式,用于向上转型为 Any,然后允许向下转型为具体类型。它还支持 Box。https://crates.io/crates/downcast-rs - sffc
显示剩余2条评论

29
截至2017年6月,“子特征强制”(或“超特征强制”)的状态如下:
- 一份被接受的RFC #0401 将其作为强制的一部分进行了提及。因此,此转换应该是隐式完成的。

coerce_inner(T) = U,其中TU的子特征;

- 然而,这还没有实现。有一个相应的问题 #18600
此外,还有一个重复的问题 #5665。那里的评论解释了阻止此功能实现的原因。
  • 基本上,问题是如何为超级trait导出虚函数表。当前的虚函数表布局如下(以x86-64为例):
    +-----+-------------------------------+
    | 0- 7|指向“drop glue”函数的指针        |
    +-----+-------------------------------+
    | 8-15|数据的大小                      |
    +-----+-------------------------------+
    |16-23|数据的对齐方式                  |
    +-----+-------------------------------+
    |24-  |Self和supertraits的方法          |
    +-----+-------------------------------+
    
    它不包含超级trait作为子序列的虚函数表。我们至少需要对虚函数表进行一些调整。
  • 当然,有许多缓解这个问题的方法,但每种方法都有不同的优点/缺点!其中一种在存在菱形继承时可以减小虚函数表的大小。另一种则被认为更快。

@typelist中提到,他们准备了一个组织良好的草案RFCa draft RFC,但之后他们好像消失了(2016年11月)。


这现在在夜版上运行(在 #![feature(trait_upcasting)] 下),甚至成为稳定的候选!但是有一些关于可执行文件大小的担忧。 - Chayim Friedman

23

当我开始学习Rust时,我也遇到了同样的问题。

trait X: Y {} 的意思是,当你为结构体 S 实现trait X 时,你也需要S 实现trait Y

当然,这意味着一个 &X 知道它也是一个 &Y,因此提供了相应的函数。如果您需要先遍历指向Y的vtable指针,则需要一些运行时开销(更多的指针解引用)。

不过,当前的设计加上其他vtable指针可能不会造成太大的负担,并且可以实现简单的类型转换。所以也许我们两者都需要?这是在internals.rust-lang.org上讨论的事情。


你能开发吗?尤其是如何处理或解决不同设计的问题?我在 Rust 编程的前两个小时也遇到了相同的问题 :/ - sandwood

2

1

现在它可以在稳定的Rust上运行,您可以向上转型到基本trait,也可以直接从派生trait对象调用基本trait函数

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);
}

游乐场链接


稳定版 Rust 的版本是多少? - Craig McQueen
@CraigMcQueen 我不确定最低版本,但上面的游乐场链接显示它可以在1.64.0上运行。 - Mohammed Essehemy
2
预期的功能还没有稳定(尚未)。想要的是从&dyn Derived&dyn Base的转换。从&impl Derived&impl Base的转换一直是允许的。 - Chayim Friedman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接