简体   繁体   English

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

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

What's the correct way to use a trait object that contains a method that returns a reference to Self ?使用包含返回Self引用的方法的 trait 对象的正确方法是什么? The following code以下代码

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

gives the compiler error给出编译器错误

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

Now, we can resolve this in at least one of two ways.现在,我们可以通过两种方式中的至少一种来解决这个问题。 Either, we can modify the definition of gen to:或者,我们可以将gen的定义修改为:

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

or, we can modify the definition of bar to:或者,我们可以将 bar 的定义修改为:

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

That said, I don't understand the difference between the two and what circumstance either should be used or if another method should be used.也就是说,我不明白两者之间的区别以及应该使用什么情况或是否应该使用另一种方法。

The key piece here is to understand the cause of the error itself.这里的关键是了解错误本身的原因。 With your function用你的功能

fn bar(f : &dyn Foo) {

it would be expected that you could call f.gen() (given the current definition of Foo ), however that can't be supported because we don't known what type it would return!预计您可以调用f.gen() (鉴于Foo的当前定义),但是无法支持,因为我们不知道它将返回什么类型! In the context of your specific code, it could be either A or B and in the general case, anything could implement that trait.在您的特定代码的上下文中,它可以是AB ,在一般情况下,任何东西都可以实现该特征。 That is why this gives这就是为什么这给

the trait Foo cannot be made into an object特征Foo不能变成一个对象

If it could be made into a trait object, code that tries to use the reference to the object wouldn't be well-defined, like f.gen() .如果它可以被做成一个 trait 对象,那么尝试使用对对象的引用的代码就不会被很好地定义,比如f.gen()

Now, we can resolve this in at least one of two ways.现在,我们可以通过两种方式中的至少一种来解决这个问题。 I don't understand the difference between the two and what circumstance either should be used or if another method should be used.我不明白两者之间的区别以及应该使用什么情况或是否应该使用另一种方法。

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

    This function, because it now has a limit on Self , actually can't be used by your bar function, because dyn Foo is not Sized .这个函数,因为它现在对Self有限制,实际上不能被你的bar函数使用,因为dyn Foo不是Sized If you put that limit in place and try to call f.gen() inside bar you will get the error如果您设置该限制并尝试在bar内调用f.gen()您将收到错误

    the gen method cannot be invoked on a trait object不能在 trait 对象上调用gen方法

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

    This approach addresses the issue because we actually do know what type f.gen() would return ( F ).这种方法解决了上述问题,因为我们实际上知道什么类型f.gen()将返回( F )。 Also note that this can be simplified to fn bar<F: Foo>(f : &F) { or even fn bar(f : &impl Foo) { .另请注意,这可以简化为fn bar<F: Foo>(f : &F) {甚至fn bar(f : &impl Foo) {

Unless you're really super optimizing for performance, at least somewhat this is your preference.除非您真的对性能进行了超级优化,否则至少在某种程度上这是您的偏好。 Would you prefer to pass a trait object, or need <F> on every function the object is passed to?你更喜欢传递一个 trait 对象,还是需要在传递给对象的每个函数上都使用<F>

More technical answer:更多技术答案:

On the technical side, which you probably don't need to worry about, the tradeoff here is performance vs executable code size.在技​​术方面,您可能不需要担心,这里的权衡是性能与可执行代码大小。

Your generic bar<F> function, because the type F is explicitly known inside the function, will actually create multiple copies of the bar function in the compiled output executable, like if you'd instead done fn bar_A(f: &A) { and fn bar_B(f: &B) { .您的通用bar<F>函数,因为在函数内部明确知道类型F ,实际上会在编译的输出可执行文件中创建bar函数的多个副本,就像您改为执行fn bar_A(f: &A) {fn bar_B(f: &B) { . This process is called monomorphization .这个过程称为monomorphization

The upside of this process is that, because there are independent copies of the function, the compiler can optimize the function's code better, and the locations where the function is called could too, since the type of F is known ahead of time.这个过程的好处是,因为有函数的独立副本,编译器可以更好地优化函数的代码,并且调用函数的位置也可以,因为提前知道F的类型。 For instance, when you call f.eval() , bar_A will always call A::eval and bar_B will always call B::eval , and when you call bar(aa.gen());例如,当您调用f.eval()bar_A将始终调用A::evalbar_B将始终调用B::eval ,而当您调用bar(aa.gen()); , it already knows that it is calling bar_a(aa.gen()) . ,它已经知道它正在调用bar_a(aa.gen())

The downside here is that, if you had many types that implemented Foo and you call bar for all of them, you would be creating just as many copies of bar_XXX for those types.这里的缺点是,如果你有很多类型实现了Foo并且你为所有类型调用bar ,你将为这些类型创建同样多的bar_XXX副本。 That will make your final executable file larger, but potentially faster because the types where all known for the compiler to optimize and inline things.这将使您的最终可执行文件更大,但可能更快,因为编译器都知道这些类型可以优化和内联事物。

On the other hand, if you go with fn bar(f : &dyn Foo) { , these two points could end up flipped.另一方面,如果你使用fn bar(f : &dyn Foo) { ,这两个点最终可能会翻转。 Since there is only one copy of bar in the executable, it doesn't know the type referenced by f when it calls f.eval() , which means that you miss out on potential compiler optimizationas and that your function needs to do dynamic dispatch .由于可执行文件中只有一个bar副本,因此它在调用f.eval()时不知道f引用的类型,这意味着您错过了潜在的编译器优化,并且您的函数需要进行动态分派. Where f : &F knows the type F , f: &dyn Foo needs to look at metadata associated with f to figure out which trait implementation's eval to call.其中f : &F知道类型Ff: &dyn Foo需要查看与f关联的元数据,以确定要调用哪个特征实现的eval

This all means that for f: &dyn Foo , your final executable will be smaller, which could be good for RAM usage, but it could be slower if bar is called as part of the core logic loop of your application.这一切都意味着对于f: &dyn Foo ,您的最终可执行文件将更小,这可能有利于 RAM 使用,但如果bar作为应用程序核心逻辑循环的一部分被调用,它可能会更慢。

See What are the actual runtime performance costs of dynamic dispatch?请参阅动态分派的实际运行时性能成本是多少? for more explanation.更多解释。

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

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