4.7.2 为 Box 实现 Trait

译文 · 基于 Learning Rust

为 Box 实现 Trait

原文链接: https://quinedot.github.io/rust-learning/dyn-trait-box-impl.html

让我们看看如何实现 Trait for Box<dyn Trait + '_>。首先要注意的是,大多数方法都会涉及调用 box 内 dyn Trait 的方法,但若直接使用 self.method(),我们会立刻与正在编写的方法(<Box<dyn Trait>>::method)递归!

在这些情况下,我们需要调用 <dyn Trait>::method 而非 <Box<dyn Trait>>::method,以避免无限递归。

既然已强调这一点,让我们直接开始:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
trait Trait {
    fn look(&self);
    fn boop(&mut self);
    fn bye(self) where Self: Sized;
}

impl Trait for Box<dyn Trait + '_> {
    fn look(&self) {
        // 我们不想这样做!
        // self.look()

        // 那会递归调用*本*函数!
        // 我们需要调用 `<dyn Trait as Trait>::look`。

        // 以下形式均可,取决于你希望多明确。

        // 非常明确
        // <dyn Trait as Trait>::look(&**self)

        // 感谢函数参数的自动解引用
        // <dyn Trait>::look(self)

        // 非常简洁,属于「见多了就懂」的形式。
        // 第一次解引用针对引用(`&Self`),第二次针对 `Box<_>`。
        (**self).look()
    }

    fn boop(&mut self) {
        // 与 `&self` 情况类似
        (**self).boop()
    }

    fn bye(self) {
        // 呃……见下文
    }
}

哦对了,最后一个。还记得我们之前说的吗? dyn Trait 没有此方法,但 Box<dyn Trait + '_> 有。 编译器不会在这里猜测该怎么做(而且若需要返回值也无法猜测)。我们无法将 dyn Trait 移出 Box,因为它是 unsized。我们也无法从 dyn Trait 向下转型;即使能,这里也很少有帮助,因为我们既要施加 'static 约束,又要知道实现我们 trait 的每种类型以便对每种尝试向下转型(或采用其他更高效的向下转型方案)。

唉,难怪 Box<dyn Trait> 不会自动实现 Trait。

假设我们想在擦除类型上调用 Trait::bye,是否无路可走?不,有变通办法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 超 trait 约束
trait Trait: BoxedBye {
    fn bye(self);
}

trait BoxedBye {
    // 与 `self: Self` 不同,这*不*隐含 `Self: Sized`,
    // 因而*会*对 `dyn BoxedBye + '_`……以及自动对 `dyn Trait + '_` 可用。
    fn boxed_bye(self: Box<Self>);
}

// 我们为所有 `Sized` 的 `Trait` 实现者实现此 trait,
// 通过拆箱并调用 `Trait::bye`
impl<T: Trait> BoxedBye for T {
    fn boxed_bye(self: Box<Self>) {
        <Self as Trait>::bye(*self)
    }
}

impl Trait for Box<dyn Trait + '_> {
    fn bye(self) {
        // 这次传递 `self` 而非 `*self`
        <dyn Trait as BoxedBye>::boxed_bye(self);
    }
}

通过添加超 trait 约束,编译器将为 dyn Trait + '_ 提供 BoxedBye 的实现。该实现将调用 Box<Erased> 的 BoxedBye 实现,其中 Erased 是擦除的基类型。这正是我们的 blanket 实现,它拆箱 Erased 并调用 Erased 的 Trait::bye。

<dyn Trait as BoxedBye>::boxed_bye 的签名中,接收者类型为 Box<dyn Trait + '_>,与 <Box<dyn Trait + '_> as Trait>::bye 的签名完全相同。因此我们得以完成 Trait for Box<dyn Trait + '_> 的实现。

在 Box<dyn Trait + '_> 上调用 Trait::bye 时的调用流程如下:

1
2
3
4
<Box<dyn Trait>>::bye      (_: Box<dyn Trait>) -- 直接传递 -->
<    dyn Trait >::boxed_bye(_: Box<dyn Trait>) -- 通过 vtable  -->
<      Erased  >::boxed_bye(_: Box<  Erased >) -- 通过拆箱   -->
<      Erased  >::bye      (_:       Erased  ) :)

任何 Sized 的 Trait 实现者都会免费获得我们对 BoxedBye 超 trait 的 blanket 实现,无需做任何特殊处理。


最后一点,我们写的是

1
2
3
4
# trait Trait {}
impl Trait for Box<dyn Trait + '_> {
// 这里:                     ^^^^
# }

我们不需要要求 'static,因此更灵活。 也很容易忘记。

最后修改 August 23, 2026: 更新 (499855b16)