【问题标题】:Why doesn't Rust support trait object upcasting?为什么 Rust 不支持 trait 对象向上转换?
【发布时间】:2021-05-10 01:27:51
【问题描述】:

鉴于此代码:

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,它可能驻留在堆栈中,这并不是一个优雅(或最佳)的解决方案。

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

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

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

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

【问题讨论】:

  • 附带说明:Rust 不是面向对象的语言。 Traits 不是接口,它们更像是 Haskell 的类型类。 Rust 也没有子类型,所以 LSP 有点不适用于它,因为它的定义与子类型关系相关。
  • 不过,正如我所说,Rust 支持许多 OO 风格的抽象,并且允许继承特征,形成类似于类型层次结构的东西。对我来说,支持特征对象的 LSP 似乎很自然,即使 OO 不是该语言的主要范式。
  • 如果它解决了您的问题,请确保为有用的答案投票,并将答案标记为已接受!如果没有可接受的答案,请考虑让 cmets 解释原因,或编辑您的问题以不同的方式表述问题。
  • 这个问题有一个 Rust 问题:github.com/rust-lang/rust/issues/5665(我看到你已经找到了;只是在这里放置一个链接。)
  • 你是如何得到这个 LLVM IR 的?

标签: oop rust language-design liskov-substitution-principle


【解决方案1】:

当我开始使用 Rust 时,我遇到了同样的问题。 现在,当我想到特质时,我脑海中的形象与我想到类时不同。

trait X: Y {} 意味着当你为 struct S 实现 trait X 时,你还需要S 实现 trait Y

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

再说一次,当前的设计 + 指向其他 vtable 的附加指针可能不会造成太大影响,并且可以轻松实现强制转换。所以也许我们两者都需要?这是要在internals.rust-lang.org讨论的事情

【讨论】:

  • 你会开发吗?特别是如何处理或解决不同的设计?在我的前两个小时的 rust 编码之后,我正好碰到了同一堵墙:/
【解决方案2】:

其实,我想我明白了。我找到了一种优雅的方法来为任何需要它的特征添加向上转换的支持,这样程序员就可以选择是否将额外的 vtable 条目添加到特征中,或者不喜欢,这是一个类似的权衡C++ 的虚拟与非虚拟方法:优雅和模型正确性与性能。

代码可以如下实现:

trait Base: AsBase {
    // ...
}

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

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}

可以添加额外的方法来转换&amp;mut 指针或Box(这增加了T 必须是'static 类型的要求),但这是一个普遍的想法。这允许安全和简单(尽管不是隐式)向上转换每个派生类型,而无需为每个派生类型提供样板。

【讨论】:

  • 不幸的是,如果使用复杂的特征架构,这种impl Trait 方法的使用范围有点窄。 play.integer32.com/… 没有消除相交实现的可能性(即impl &lt;T: AsBranchOne + !AsBranchTwo&gt;),需要将as_*() 方法直接安装到嵌套特征中以防止冲突。
  • @Shepmaster 在今天的 Rust 中应该是 AsRef&lt;Base&gt; 吗?
  • @Bergi AsRef 从 Rust 1.0 开始可用,但我不确定你是否可以在这里使用它。一些quick attempts 显示各种错误。
  • “可以添加额外的方法来转换 &mut 指针或 Box”——你能提供 Box 的例子吗?我不清楚在不使用 unsafe 的情况下如何使它在这种情况下工作。
  • @kFYatek 我的代码库中有类似的方法,但我很难让它与Box 一起工作。你已经成功了吗?
【解决方案3】:

截至 2017 年 6 月,该“子特征强制”(或“超特征强制”)的状态如下:

  • 已接受的 RFC #0401 提到这是强制的一部分。所以这个转换应该隐式完成。

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

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

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

  • 基本上,问题在于如何为超级特征派生 vtable。 vtables 的当前布局如下(在 x86-64 情况下):
    +-----+-------------------+
    | 0- 7|指向“滴胶”功能|
    +-----+------------------+
    | 8-15|数据大小|
    +-----+------------------+
    |16-23|数据对齐|
    +-----+------------------+
    |24- |自我和超特质的方法|
    +-----+------------------+
    
    它不包含作为子序列的超级特征的 vtable。我们至少需要对 vtable 进行一些调整。
  • 当然有一些方法可以缓解这个问题,但是很多方法都有不同的优点/缺点!当有菱形继承时,对 vtable 大小有好处。另一个应该更快。

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

【讨论】:

    【解决方案4】:

    此功能非常受欢迎,以至于将其添加到语言中存在跟踪问题,并且为实现该功能做出贡献的人提供了一个专用的倡议存储库。

    跟踪问题:https://github.com/rust-lang/rust/issues/65991

    主动存储库:https://github.com/rust-lang/dyn-upcasting-coercion-initiative

    【讨论】:

      猜你喜欢
      • 2017-05-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-29
      • 1970-01-01
      相关资源
      最近更新 更多