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

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

给定这段代码:

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 都包含一个指向析构函数的指针,在第一个字段中的大小和对齐方式,并且 subtrait vtables 在引用 supertrait 方法时不会复制它们,也不会使用对 supertrait vtables 的间接引用。他们只有方法指针的逐字副本,没有别的。

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

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

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

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

有谁知道这样设计的原理吗?

最佳答案

其实,我想我明白了原因。我找到了一种优雅的方式来为任何需要它的特征添加向上转换支持,这样程序员就可以选择是否将额外的 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 类型),但这是一个普遍的想法。这允许对每个派生类型进行安全和简单(尽管不是隐含的)向上转换,而无需为每个派生类型提供样板文件。

关于oop - 为什么 Rust 不支持特征对象向上转换?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50004127/

相关文章:

c++ - 通常方法上的 Woverloaded-virtual 警告

javascript - 为什么 some_func(…) != some_func.call(this, …) 在构造函数中

rust - 在字符串上创建闭包返回迭代器

rust - 在采用 &self 或 &mut self 的函数中进行模式匹配时,如何避免 ref 关键字?

c# - 新对象过时了吗?

objective-c - 还有哪些其他编程语言具有类似 Smalltalk 的消息传递语法?

C++ 接口(interface)错误;标识符 "class"未定义

oop - 多重继承的用途?

error-handling - 为什么标准错误需要在main中装箱,而不是io错误?

java - 为什么 Java 不允许 Throwable 的泛型子类?