为什么具有泛型类型参数的特征方法是对象不安全的?

Why are trait methods with generic type parameters object-unsafe?

引用the Book(强调我的),

The same is true of generic type parameters that are filled in with concrete type parameters when the trait is used: the concrete types become part of the type that implements the trait. When the type is forgotten through the use of a trait object, there is no way to know what types to fill in the generic type parameters with.

我无法理解其中的原理。对于具体示例,请考虑以下内容

pub trait Echoer {
    fn echo<T>(&self, v: T) -> T;
}

pub struct Foo { }

impl Echoer for Foo {
    fn echo<T>(&self, v: T) -> T {
        println!("v = {}", v);
        return v;
    }
}

pub fn passthrough<T>(e: Box<dyn Echoer>, v: T) {
    return e.echo(v);
}

fn main() {
    let foo = Foo { };
    passthrough(foo, 42);
}

结果当然是错误的

$ cargo run
   Compiling gui v0.1.0 (/tmp/gui)
error[E0038]: the trait `Echoer` cannot be made into an object
  --> src/main.rs:14:27
   |
14 | pub fn passthrough<T>(e: Box<dyn Echoer>, v: T) {
   |                           ^^^^^^^^^^^^^^^ `Echoer` cannot be made into an object
   |
   = help: consider moving `echo` to another trait
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:2:8
   |
1  | pub trait Echoer {
   |           ------ this trait cannot be made into an object...
2  |     fn echo<T>(&self, v: T) -> T;
   |        ^^^^ ...because method `echo` has generic type parameters

error: aborting due to previous error

For more information about this error, try `rustc --explain E0038`.
error: could not compile `gui`

To learn more, run the command again with --verbose.

根据我的理解,即使 e 在被转换为 trait 对象时忘记了它的具体类型,它仍然可以推断它需要用 echo<T> 填充泛型类型参数 i32,因为它是在 passthrough<T> 内部调用的,在编译时单态化为 passthrough<i32>

“具体类型成为实现特征的类型的一部分”是什么意思?为什么特征方法不能在编译时填充它们的泛型类型参数,例如只需调用 echo<i32>?

这与 类似,但我将在此处详细说明。

Rust 特征对象是 implemented using a vtable

当Rust编译代码如

pub fn passthrough<T>(e: Box<dyn Echoer>, v: T) {
    return e.echo(v);
}

它需要决定调用什么 echo 函数。 Box 基本上是指向值的指针,对于您的代码,Foo 将存储在堆上,Box<Foo> 将是指向 [=13 的指针=].如果您随后将其转换为 Box<dyn Echoer>,则新 Box 实际上包含 两个 指针,一个指向堆上的 Foo,一个指向 [=34] =].这个 vtable 让 Rust 在看到 e.echo(v) 时知道该做什么。 e.echo(v) 调用的编译输出将查看 vtable 以找到 e 指向的任何类型的 echo 实现,然后调用它,在这种情况下传递 Foo &self.

的指针

在简单函数的情况下,这部分很容易,但由于 fn echo<T>(&self, v: T) -> T;<T> 部分,这里的复杂性和问题就来了。模板函数本质上旨在使用单个定义声明 多个 函数,但是如果需要 vtable,它应该包含什么?如果您的特征包含一个方法,该方法具有 <T> 之类的类型参数,其中可能需要未知数量的 T 类型。这意味着 Rust 需要要么禁止引用带有类型参数的函数的 vtable,要么它需要提前预测可能需要的每个可能的 T 类型,并将其包含在 vtable 中。 Rust 遵循第一个选项并抛出您所看到的编译器错误。

虽然在某些情况下提前知道完整的 T 类型集是可能的,并且对于在小型代码库中工作的程序员来说可能看起来很清楚,但它会非常复杂并且可能会变得非常大vtables 在任何非平凡的情况下。它还需要 Rust 全面了解您的整个应用程序才能正确编译。这至少会大大减慢编译时间。

例如,Rust 通常将依赖项与您的主代码分开编译,并且在您编辑自己的项目代码时不需要重新编译您的依赖项。如果您需要提前知道所有 T 类型以生成 vtable,则需要在决定使用哪些 T 值之前处理所有依赖项和您自己的所有代码,然后才编译函数模板.同样,假设依赖项包含像您问题中的示例这样的代码,每次您更改自己的项目时,Rust 都必须检查您的更改是否引入了对具有以前未使用过的类型参数的函数的动态调用,然后它还需要重新编译依赖项,以便使用新引用的函数创建一个新的 vtable。

至少,它会引入大量额外的复杂性。

trait 对象基本上是一个胖指针,包含两个指针,一个指向对象,另一个指向包含所有方法的 vtable,因此从 trait 对象调用 echo 方法就像

trait_object.vtable.echo(trait_object.obj, "hello")

假设echo可以泛型,那么在trait对象上建立vtable时,可能会有echo_string、echo_uint等,所有可能的类型都要枚举出来。当派发方法时,它必须检查参数的类型并从 vtable 中找到实际的方法,比如

trait_object.vtable.echo_string(trait_object.obj, "hello")

该方法可能有无限组合。当调度方法时,它应该根据 T

的具体类型从 vtable 的所有可能方法中找到正确的方法