繁体   English   中英

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

[英]How to use a trait object that contains a method that returns a reference to Self?

使用包含返回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的当前定义),但是无法支持,因为我们不知道它将返回什么类型! 在您的特定代码的上下文中,它可以是AB ,在一般情况下,任何东西都可以实现该特征。 这就是为什么这给

特征Foo不能变成一个对象

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

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

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

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

    不能在 trait 对象上调用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::evalbar_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知道类型Ff: &dyn Foo需要查看与f关联的元数据,以确定要调用哪个特征实现的eval

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM