如何使用包含返回对 Self 的引用的方法的 trait 对象?



使用包含返回对Self引用的方法的 trait 对象的正确方法是什么? 以下代码

trait Foo {
fn gen(&mut self) -> &Self;
fn eval(&self) -> f64;
}
struct A {
a : f64,
}
impl Foo for A {
fn gen(&mut self) -> &Self {
self.a = 1.2;
self
}
fn eval(&self) -> f64 {
self.a + 2.3
}
}
struct B;
impl Foo for B {
fn gen(&mut self) -> &Self {
self
}
fn eval(&self) -> f64 {
3.4
}
}
fn bar(f : &dyn Foo) {
println!("Result is : {}",f.eval());
}
fn main() {
let mut aa = A { a : 0. };
bar(aa.gen());
let mut bb = B;
bar(bb.gen());
}

给出编译器错误

error[E0038]: the trait `Foo` cannot be made into an object
--> src/main.rs:30:1
|
3  |     fn gen(&mut self) -> &Self;
|        --- method `gen` references the `Self` type in its parameters or return type
...
30 | fn bar(f : &dyn Foo) {
| ^^^^^^^^^^^^^^^^^^^^ the trait `Foo` cannot be made into an object

现在,我们可以通过以下两种方式中的至少一种来解决此问题。 或者,我们可以将gen的定义修改为:

trait Foo {
fn gen(&mut self) -> &Self where Self : Sized;
fn eval(&self) -> f64;
}

或者,我们可以将 bar 的定义修改为:

fn bar<F>(f : &F) where F : Foo + ?Sized {
println!("Result is : {}",f.eval());
}

也就是说,我不明白两者之间的区别,以及应该使用什么情况或是否应该使用另一种方法。

这里的关键是了解错误本身的原因。使用您的函数

fn bar(f : &dyn Foo) {

预计您可以调用f.gen()(鉴于Foo的当前定义(,但是这不能得到支持,因为我们不知道它会返回什么类型!在您的特定代码上下文中,它可以是A的,也可以是B的,在一般情况下,任何东西都可以实现该特征。这就是为什么这给了

不能将Foo特征变成对象

如果它可以变成一个 trait 对象,那么尝试使用该对象的引用的代码就不会像f.gen()一样得到很好的定义。

现在,我们可以通过以下两种方式中的至少一种来解决此问题。我不明白两者之间的区别以及应该使用什么情况或是否应该使用另一种方法。

  1. fn gen(&mut self) -> &Self where Self : Sized;

    这个函数,因为它现在对Self有限制,其实不能被你的bar函数使用,因为dyn Foo不是Sized。如果您设置了该限制并尝试在bar内部调用f.gen(),则会出现错误

    不能在特征对象上调用gen方法

  2. fn bar<F>(f : &F) where F : Foo + ?Sized {

    这种方法解决了这个问题,因为我们实际上知道f.gen()会返回什么类型(F(。另请注意,这可以简化为fn bar<F: Foo>(f : &F) {甚至fn bar(f : &impl Foo) {.

除非您真的对性能进行了超级优化,否则至少在某种程度上这是您的偏好。您是希望传递 trait 对象,还是需要对对象传递到的每个函数进行<F>

更多技术答案:

在技术方面,您可能不需要担心,这里的权衡是性能与可执行代码大小。

您的泛型bar<F>函数,因为函数内部明确知道F类型,因此实际上将在编译的输出可执行文件中创建bar函数的多个副本,就像您改为执行fn bar_A(f: &A) {fn bar_B(f: &B) {一样。此过程称为monomorphization

这个过程的好处是,由于存在函数的独立副本,编译器可以更好地优化函数的代码,并且调用函数的位置也可以,因为F的类型是提前知道的。例如,当你呼叫f.eval()时,bar_A将始终呼叫A::eval,而bar_B将始终呼叫B::eval,而当您呼叫bar(aa.gen());时,它已经知道它正在呼叫bar_a(aa.gen())

这里的缺点是,如果您有许多实现Foo的类型,并且您为所有类型调用bar,您将为这些类型创建同样多的bar_XXX副本。这将使您的最终可执行文件更大,但可能更快,因为编译器已知所有类型都可以优化和内联内容。

另一方面,如果你和fn bar(f : &dyn Foo) {一起去,这两点最终可能会翻转。由于可执行文件中只有一个bar副本,因此它在调用f.eval()时不知道f引用的类型,这意味着您错过了潜在的编译器优化,并且您的函数需要执行动态调度。f : &F知道F的类型,f: &dyn Foo需要查看与f相关的元数据,以确定调用哪个特征实现的eval

这一切都意味着,对于f: &dyn Foo,您的最终可执行文件将更小,这可能有利于 RAM 的使用,但如果bar作为应用程序的核心逻辑循环的一部分调用,则可能会更慢。

有关更多说明,请参阅动态调度的实际运行时性能成本是多少?

最新更新