generics - 为什么具有泛型类型参数的 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>
?
解决方案
这类似于为什么 trait 中的泛型方法需要调整 trait 对象的大小?但我会在这里详细说明。
当 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。
至少,它会引入大量额外的复杂性。
推荐阅读
- apache - 试图在互联网上托管我的本地主机。我在这里想念什么?
- .net-core - .NET Core 2.2 发布因缺少 *.pdb 和 *.xml 而失败
- spring - 在 DTO 中我们可以使用 id 作为 String 还是 Long
- c# - C#中的#符号是什么
- python - 如何将 CSV 导入到两个使用 Python 相互引用的不同 SQL 表中
- vue.js - 无法安装 vuetify
- java - 如何检查一个类是否依赖 CDI 来创建实例?
- typescript - 具有客户端实用程序定义结构的基本 TypeScript NodeJS 程序,使用严格的编码规则
- python - 无法导入 .py 文件,没有名为“文件名”的模块错误
- python - 摆脱嵌套的for循环