首页 > 解决方案 > 为什么具有泛型类型参数的 trait 方法是对象不安全的?

问题描述

引用这本书(强调我的),

使用 trait 时用具体类型参数填充的泛型类型参数也是如此:具体类型成为实现 trait 的类型的一部分。当通过使用 trait 对象忘记类型时,无法知道使用什么类型填充泛型类型参数。

我无法理解其中的原理。对于一个具体的例子,考虑以下

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 对象时忘记了它的具体类型,它仍然可以推断出它需要填充 with 的泛型类型参数echo<T>i32因为它被称为 inside ,它在编译时passthrough<T>被单态化为。passthrough<i32>

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

标签: genericsrustpolymorphismtraitstrait-objects

解决方案


这类似于为什么 trait 中的泛型方法需要调整 trait 对象的大小?但我会在这里详细说明。

Rust 特征对象是使用vtable实现的胖指针

当 Rust 编译诸如

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

它需要决定echo调用什么函数。ABox基本上是一个指向值的指针,就您的代码而言, aFoo将存储在堆中,而 aBox<Foo>将是指向 a 的指针Foo。如果您随后将其转换为 a Box<dyn Echoer>,则新 Box 实际上包含两个指针,一个指向Foo堆上的指针,一个指向vtable。这个 vtable 允许 Rust 在看到e.echo(v). 您调用的编译输出e.echo(v)将查看 vtable 以找到echo任何类型e指向的实现,然后调用它,在这种情况下传递Foo指针 for &self

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

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

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

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


推荐阅读