为什么 Rust 不支持 trait 对象向上转换? [英] Why doesn't Rust support trait object upcasting?

查看:29
本文介绍了为什么 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:

Unfortunately, I cannot cast &Derived to &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 方法.

Why is that? The Derived vtable has to reference the Base methods in one way or another.

检查 LLVM IR 会发现以下内容:

Inspecting the LLVM IR reveals the following:

@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 的间接引用.他们只是逐字复制了方法指针,没有别的.

All Rust vtables contain a pointer to the destructor, size and alignment in the first fields, and the subtrait vtables don't duplicate them when referencing supertrait methods, nor use indirect reference to supertrait vtables. They just have copies of the method pointers verbatim and nothing else.

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

Given that design, it's easy to understand why this does not work. A new vtable would need to be constructed at runtime, which would likely reside on the stack, and that isn't exactly an elegant (or optimal) solution.

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

There are some workarounds, of course, like adding explicit upcast methods to the interface, but that requires quite a bit of boilerplate (or macro frenzy) to work properly.

现在,问题是 - 为什么不以某种方式实现它以启用 trait 对象向上转换?比如,在 subtrait 的 vtable 中添加一个指向 supertrait 的 vtable 的指针.目前,Rust 的动态调度似乎并不满足Liskov 替换原则,这是一个非常面向对象设计的基本原则.

Now, the question is - why isn't it implemented in some way that would enable trait object upcasting? Like, adding a pointer to the supertrait's vtable in the subtrait's vtable. For now, Rust's dynamic dispatch doesn't seem to satisfy the Liskov substitution principle, which is a very basic principle for object-oriented design.

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

Of course you can use static dispatch, which is indeed very elegant to use in Rust, but it easily leads to code bloat which is sometimes more important than computational performance - like on embedded systems, and Rust developers claim to support such use cases of the language. Also, in many cases you can successfully use a model which is not purely Object-Oriented, which seems to be encouraged by Rust's functional design. Still, Rust supports many of the useful OO patterns... so why not the LSP?

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

Does anyone know the rationale for such design?

推荐答案

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

Actually, I think I got the reason. I found an elegant way to add upcasting support to any trait that desires it, and that way the programmer is able to choose whether to add that additional vtable entry to the trait, or prefer not to, which is a similar trade-off as in C++'s virtual vs. non-virtual methods: elegance and model correctness vs. performance.

代码可以实现如下:

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 必须是 T 的要求code>'static 类型),但这是一个总体思路.这允许对每个派生类型进行安全和简单(虽然不是隐式)向上转换,而无需为每个派生类型提供样板.

One may add additional methods for casting a &mut pointer or a Box (that adds a requirement that T must be a 'static type), but this is a general idea. This allows for safe and simple (although not implicit) upcasting of every derived type without boilerplate for every derived type.

这篇关于为什么 Rust 不支持 trait 对象向上转换?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆