考虑下面的代码:
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 呢?
有没有人知道这种设计的原理?