【问题标题】:How to use a trait object that contains a method that returns a reference to Self?如何使用包含返回 Self 引用的方法的 trait 对象?
【发布时间】:2020-01-01 00:23:15
【问题描述】:

使用包含返回对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());
}

给出编译器错误

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

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

【问题讨论】:

    标签: rust


    【解决方案1】:

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

    fn bar(f : &dyn Foo) {
    

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

    特征Foo 不能被制成对象

    如果可以将其制成一个 trait 对象,那么尝试使用对该对象的引用的代码将不会得到良好定义,例如 f.gen()

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

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

      这个函数,因为它现在对Self有限制,实际上不能被你的bar函数使用,因为dyn Foo Sized。如果您设置该限制并尝试在bar 内调用f.gen(),您将收到错误

      gen 方法不能在 trait 对象上调用

    2. fn bar&lt;F&gt;(f : &amp;F) where F : Foo + ?Sized {

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

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

    更多技术答案:

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

    您的通用 bar&lt;F&gt; 函数,因为 F 类型在函数内部是明确已知的,实际上将在编译的输出可执行文件中创建 bar 函数的多个副本,就像您改为完成 fn bar_A(f: &amp;A) {fn bar_B(f: &amp;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 : &amp;dyn Foo) {,这两点可能会颠倒过来。由于可执行文件中只有一个bar 的副本,因此它在调用f 时不知道f 引用的类型,这意味着您错过了潜在的编译器优化以及您的函数需要做的事情dynamic dispatch。其中f : &amp;F 知道F 的类型,f: &amp;dyn Foo 需要查看与f 关联的元数据,以确定要调用哪个特征实现的eval

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

    更多解释请见What are the actual runtime performance costs of dynamic dispatch?

    【讨论】:

      猜你喜欢
      • 2021-11-11
      • 2021-01-18
      • 2012-05-04
      • 1970-01-01
      • 2018-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-23
      相关资源
      最近更新 更多