【发布时间】: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