为什么要使用装箱对象而不是特质对象?

3
在《Rust for Rustaceans》一书中,作者写道:
总的来说,在库中你应该使用静态分发,在二进制文件中你应该使用动态分发。在库中,你希望允许用户决定哪种分发方式最适合他们,因为你不知道他们的需求是什么。
我猜,在二进制情况下,他指的是这个:
fn flexible_dispatch_method(_: &dyn MyTrait) {}

// static dispatch
//
let obj = MyType {};
flexible_dispatch_method(&obj);

// dynamic dispatch
//
let trait_obj: &dyn MyTrait = &MyType {};
flexible_dispatch_method(trait_obj);

鉴于上述内容,使用装箱对象而不是特质对象的优势在哪里?这是因为需要使用生命周期吗?
fn return_new_trait_object<'a>() -> &'a dyn MyTrait {
    &MyType {}
}

还是有其他的情况吗?根据我的理解,在动态分配的情况下,对象需要在堆中进行分配,因此我认为与装箱对象相比没有太大的区别。


除了这个很好的答案之外,让我指出返回 &MyType {} 是无法编译的。你只能返回先前接收到的数据的引用,而不能返回新创建的数据的引用。尝试做后者会创建一个悬空引用,并被借用检查器阻止。 - user4815162342
@user4815162342 从技术上讲,由于静态提升的原因,在这种情况下它应该可以编译。但在一般情况下,可能不行。 - Chayim Friedman
1个回答

12

我认为你可能对这里的一些事情有误解。

  • (通常)静态分配发生在你调用一个具体类型上的方法时,该类型不是 dyn Trait。这是编译器在编译时决定要调用哪个函数的时候发生的。
  • 特质对象是指任何指向 dyn Trait 的指针,包括 Box<dyn Trait>&dyn TraitArc<dyn Trait> 等等。当你通过 dyn Trait 调用函数时,编译器会插入代码来查找要在运行时调用的函数,从而实现多态和更大的灵活性。

在你的代码中,flexible_dispatch_method(_: &dyn MyTrait) 总是使用动态分配,因为它的参数类型为 &dyn MyTrait。以下是静态分配的示例:

fn flexible_dispatch_method<T: MyTrait + ?Sized>(_: &T) {}

使用这个声明,您的第一个用法将使用静态调度,第二个用法将使用动态调度。
动态分派更加灵活,因为它避免了在各个地方都有泛型。这对于编写大型应用程序非常有用,因为您可能希望具有多态性并轻松添加新实现。但是,动态调度会带来一些性能损失,因此库应尽可能让调用者选择调度方式。
关于何时使用“&dyn Trait”和“Box”:它们都基于所有权。如果您想要一个拥有的trait对象,请使用“Box”,如果您想要一个借用的trait对象,请使用“&dyn Trait”。

谢谢!是的,我对这本书感到非常困惑。 - Marcus
2
我猜你在谈论Jon Gjengset的那本书(至少是同名书)?我记得他提到这本书主要面向已经了解Rust并想要深入学习的人群。这并不是说你不应该读它,我非常尊重Jon的工作,我相信这本书一定很棒,只是在阅读Rust书籍以更熟悉语言后,你可能会从中获得更多收获。如果你遇到困难,Rust社区总是在这里帮助你! - Ian S.
1
动态分发更加灵活,因为它避免了在各个地方使用泛型。这是它更加“方便”的一个论点,而不是更加“灵活”。如果说有什么区别的话,静态分发更加灵活,因为它可以用于非对象安全特性,并且可以对关联类型和常量等进行泛型处理。我甚至不确定dyn是否更加方便:impl Trait并不比dyn Trait差,也不需要间接引用。然而,两者都有其用途,在某些情况下需要使用dyn Trait - Chayim Friedman
@IanS。谢谢你的建议,但实际上我已经阅读了几本 Rust 书籍,并且在一些非平凡的 Rust 项目中有经验(但我不是专业的 Rust 程序员)。我的印象是 Gjengset 的书是一本高级书籍(足够公正),而不是所声称的中级书籍。当然,这是主观的,但我发现这本书与市场上的许多其他书籍之间存在着显著的差距。 - Marcus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接