nal*_*zok 12 generics polymorphism traits rust trait-objects
引用这本书(强调我的),
当使用特征时,用具体类型参数填充的泛型类型参数也是如此:具体类型成为实现该特征的类型的一部分。当通过使用特征对象而忘记类型时,就无法知道用什么类型填充泛型类型参数。
我无法理解其中的道理。对于具体示例,请考虑以下内容
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);
}
Run Code Online (Sandbox Code Playgroud)
结果当然是报错
$ 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.
Run Code Online (Sandbox Code Playgroud)
根据我的理解,即使在e转换为特征对象时忘记了它的具体类型,它仍然可以推断它需要填充 with 的泛型类型参数echo<T>,i32因为它被称为 inside ,它在编译时passthrough<T>被单态化。passthrough<i32>
“具体类型成为实现该特征的类型的一部分”是什么意思?为什么特征方法不能在编译时填充其泛型类型参数,例如仅调用echo<i32>?
log*_*yth 15
这类似于为什么特征内的通用方法需要调整特征对象的大小?但我会在这里详细说明。
当 Rust 编译如下代码时
pub fn passthrough<T>(e: Box<dyn Echoer>, v: T) {
return e.echo(v);
}
Run Code Online (Sandbox Code Playgroud)
它需要决定echo调用哪个函数。ABox基本上是一个指向值的指针,在您的代码中, aFoo将存储在堆上,并且 aBox<Foo>将是指向 a 的指针Foo。如果您随后将其转换为 a Box<dyn Echoer>,则新 Box 实际上包含两个指针,一个指向Foo堆上的指针,另一个指向vtable。这个虚函数表让 Rust 知道当它看到e.echo(v). 调用的编译输出e.echo(v)将查看 vtable 以查找指向echo的任何类型的实现e,然后调用它,在本例中传递Foo的指针&self。
对于简单函数来说,这部分很容易,但这里的复杂性和问题是<T>由于fn echo<T>(&self, v: T) -> T;. 模板函数本质上是旨在使用单个定义声明许多函数,但如果需要 vtable,它应该包含什么?如果您的特征包含一个具有类似类型参数的方法<T>,那么T可能需要的类型数量未知。这意味着 Rust 需要要么禁止 vtable 引用带有类型参数的函数,要么需要提前预测T可能需要的每种可能类型,并将其包含在 vtable 中。Rust 遵循第一个选项,并抛出像您所看到的那样的编译器错误。
虽然在某些情况下提前知道完整的T类型集是可能的,并且对于在小型代码库中工作的程序员来说似乎很清楚,但它会非常复杂,并且在任何重要情况下都可能会产生非常大的 vtable。它还需要 Rust 充分了解整个应用程序的其他方面才能正确编译。这至少会大大减慢编译时间。
例如,Rust 通常会与主代码分开编译依赖项,并且当您编辑自己的项目代码时不需要重新编译依赖项。如果您需要提前知道所有T类型来生成 vtable,则需要在决定T使用哪些值之前处理所有依赖项和您自己的所有代码,然后才编译函数模板。同样,假设依赖项包含像问题中的示例那样的代码,每次您更改自己的项目时,Rust 都必须检查您的更改是否引入了对具有以前未使用过的类型参数的函数的动态调用,然后它还需要重新编译依赖项,以便使用新引用的函数创建一个新的 vtable。
至少,它会带来大量额外的复杂性。