为什么 Rust 不支持 trait 对象向上传输?

根据这个代码:

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 vtables 都包含一个指向第一个字段中的析构函数、大小和对齐的指针,当引用 supertrait 方法时,subtrait vtables 不会重复它们,也不会间接引用 supertrait vtables。它们只有方法指针的副本,没有其他内容。

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

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

现在的问题是——为什么它没有以某种方式实现,从而启用 trait 对象向上转换?比如,在超属性的 vtable 中添加一个指向超属性的 vtable 的指针。目前,拉斯特的克劳斯·福尔曼似乎还不能满足面向对象设计的基本原则—— Liskov代换原则

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

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

17213 次浏览

当我开始和 Rust 合作的时候,我也遇到了同样的问题。 现在,当我考虑 trait 的时候,我脑海中的形象和考虑类的时候是不一样的。

trait X: Y {}意味着当你为结构 S实现 trait X时,你也为 S实现 trait Y

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

不过,当前的设计 + 指向其他 vtable 的附加指针可能不会造成太大影响,并且可以实现简单的强制转换。也许我们两个都需要?这是 Internals.rust-lang.org上要讨论的问题

事实上,我想我知道原因了。我发现了一种优雅的方法,可以为任何想要的 trait 添加向上转换支持,这样程序员就可以选择是否在 trait 中添加额外的 vtable 条目,这是一种类似于 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类型的要求) ,但这是一个大致的想法。这允许对每个派生类型进行安全和简单的(尽管不是隐式的)向上转换,而不需要为每个派生类型提供样板。

截至2017年6月,这种“亚特质胁迫”(或“超特质胁迫”)的状况如下:

  • 一个被接受的 RFC # 0401提到这是强制的一部分,所以这个转换应该隐式地进行。

    Coce _ inner (T) = U,其中 TU的一个子性状;

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

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

  • 基本上,问题是如何为 super-trait 派生 vtables:
    +-----+-------------------------------+
    | 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 作为子序列。我们至少要对 vtables 进行一些调整。
  • 当然,有一些方法可以减轻这个问题,但是许多方法都有不同的优缺点!当存在钻石继承时,对 vtable 大小有一个好处。另一个应该更快。

在那里 @ 打字员说,他们准备的 草案 RFC看起来组织良好,但他们看起来像消失后(2016年11月)。

这个特性是如此的受欢迎,以至于在将它添加到语言中时会出现跟踪问题,而且为实现它的人员提供了一个专用的计划存储库。

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

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

现在可以处理稳定的生锈了,你可以直接向上转换到基本 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);
}

游乐场连接路