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

How do Rust's Trait Objects handle methods which moves the instance?

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

foo.method();

其中 method 定义为 method(&self)fooFoo, 的实例,与

相同
Foo::method(&foo);

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

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

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

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

关键词是object safety。本质上,这个 属性 结合了两个要求:

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

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

trait Consumer {
    fn consume(self);
}

编译器已经不高兴了:

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);
  |                ^

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

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

fn main() {
    let _: Box<dyn Consumer> = todo!();
}

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

error[E0038]: the trait `Consumer` cannot be made into an object
 --> src/main.rs:6:12
  |
6 |     let _: Box<dyn Consumer> = todo!();
  |            ^^^^^^^^^^^^^^^^^ `Consumer` cannot be made into an object
  |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
 --> src/main.rs:1:28
  |
1 | trait Consumer where Self: Sized {
  |       --------             ^^^^^ ...because it requires `Self: Sized`
  |       |
  |       this trait cannot be made into an object...

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

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

...编译上面的代码。

现在,使用 这个特征对象怎么样?让我们实现它,例如,用于单位类型,并从 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();
}

另一个编译器错误!

error: the `consume` method cannot be invoked on a trait object
  --> src/main.rs:11:14
   |
2  |     fn consume(self) where Self: Sized;
   |                                  ----- this has a `Sized` requirement
...
11 |     consumer.consume();
   |              ^^^^^^^

同样,我们对方法的限制禁止了编译后毫无意义的代码。