Rust 的 Trait 对象如何处理移动实例的方法?

kil*_*s90 2 methods polymorphism vtable rust

以下是我对 Rust 方法如何工作的基本假设:

foo.method();
Run Code Online (Sandbox Code Playgroud)

其中method定义为method(&self)并且foo是 的一个实例Foo,

Foo::method(&foo);
Run Code Online (Sandbox Code Playgroud)

我对 Trait Objects 的理解是一个有两个空指针的结构,一个指向数据,另一个指向函数指针(vtable)

接受 Trait 对象并调用该 Trait 对象上的方法的多态函数将编译为查看 Trait 对象中方法的偏移量并传入数据指针

但是如果该方法移动了实例呢?如果我错了,请纠正我,但要调用虚拟移动方法,该函数必须将存储在 Trait 对象中的实际数据推送到堆栈上,而不仅仅是数据指针。显然在编译时无法知道数据大小,那么这里发生了什么?这是 VLA 类型的情况,还是我误解了移动的工作原理?

Cer*_*rus 5

答案很简单——在 trait 对象上调用自消费方法是不可能的。

关键词是对象安全。本质上,此属性结合了两个要求:

  • 每种方法都必须self通过某种间接方式使用;
  • 每个方法都必须通过原始类型(即没有泛型)完全可识别。

为了更详细地了解这一点,让我们实际尝试编写一些代码并询问编译器的意见。首先,只是试图定义特征:

trait Consumer {
    fn consume(self);
}
Run Code Online (Sandbox Code Playgroud)

编译器已经不高兴了:

trait Consumer {
    fn consume(self);
}
Run Code Online (Sandbox Code Playgroud)

好的,我们可以比编译器的建议更保守,并添加对 trait 的限制。然后,为 trait 对象创建添加一个存根:

trait Consumer where Self: Sized {
    fn consume(self);
}

fn main() {
    let _: Box<dyn Consumer> = todo!();
}
Run Code Online (Sandbox Code Playgroud)

现在,错误稍微复杂一些:

error[E0277]: the size for values of type `Self` cannot be known at compilation time
 --> src/lib.rs:2:16
  |
2 |     fn consume(self);
  |                ^^^^ doesn't have a size known at compile-time
  |
help: consider further restricting `Self`
  |
2 |     fn consume(self) where Self: Sized;
  |                      ^^^^^^^^^^^^^^^^^
help: function arguments must have a statically known size, borrowed types always have a known size
  |
2 |     fn consume(&self);
  |                ^
Run Code Online (Sandbox Code Playgroud)

然而,有一个解决方法:没有必要限制整个特征——只是有问题的方法,正如我们从一开始就被告知的那样。移动where子句,如下:

trait Consumer {
    fn consume(self) where Self: Sized;
}
Run Code Online (Sandbox Code Playgroud)

...使上面的代码编译。

现在,实际使用这个 trait 对象怎么样?例如,让我们为单位类型实现它,并使用它 from main

trait Consumer {
    fn consume(self) where Self: Sized;
}

impl Consumer for () {
    fn consume(self) {}
}

fn main() {
    let consumer: Box<dyn Consumer> = Box::new(());
    consumer.consume();
}
Run Code Online (Sandbox Code Playgroud)

另一个编译器错误!

trait Consumer where Self: Sized {
    fn consume(self);
}

fn main() {
    let _: Box<dyn Consumer> = todo!();
}
Run Code Online (Sandbox Code Playgroud)

同样,我们对方法施加的限制禁止代码,如果它被编译将毫无意义。