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

wye*_*r33 3 rust

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

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());
}

Run Code Online (Sandbox Code Playgroud)

给出编译器错误

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

log*_*yth 6

这里的关键是了解错误本身的原因。用你的功能

fn bar(f : &dyn Foo) {
Run Code Online (Sandbox Code Playgroud)

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

特性Foo不能被做成一个对象

如果它可以被做成一个 trait 对象,那么尝试使用该对象的引用的代码就不会被很好地定义,比如f.gen().

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

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

    此功能,因为它现在有一个限制Self,实际上无法通过您的使用bar功能,因为dyn Foo没有 Sized。如果您设置该限制并尝试在f.gen()内部调用,bar您将收到错误

    gen无法在 trait 对象上调用该方法

  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在编译的输出可执行文件中创建该函数的多个副本,就像您改为 donefn 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调用时引用的类型f.eval(),这意味着您错过了潜在的编译器优化,并且您的函数需要执行动态分派。Wheref : &F知道 type Ff: &dyn Foo需要查看与关联的元数据f以确定eval要调用哪个特征实现。

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

请参阅动态分派的实际运行时性能成本是多少?更多解释。