4.2 dyn Trait 实现
5 分钟阅读
译文 · 基于 Learning Rust
dyn Trait 实现
原文链接: https://quinedot.github.io/rust-learning/dyn-trait-impls.html
为使 dyn Trait 能对实现 Trait 的基础类型进行抽象, dyn Trait 本身需要实现 Trait。编译器提供该实现。此处我们概览其概念上的工作方式,并触及由此产生的一些相关限制。
我们还会涉及与 dyn Trait 对 Trait 的实现如何工作(或不工作)相关的一些令人意外的边角情况。
dyn Trait 如何实现 Trait
首先说明:这只是粗略草图,非规范性。编译器实际做法是实现细节。但通过提供可能如何实现的草图,我们希望帮助理解 dyn Trait 作为具体类型,并解释 dyn Trait 的限制。
免责声明之后,看编译器实现可能的样子:
| |
处理 dyn Trait 时,你会处理指向擦除基础类型的指针和 vtable。例如,可想象 &dyn Trait 类似:
| |
这里用瘦 *const () 指向擦除的基础类型。类似地,可想象 DynTraitMut<'a> 对应 &'a mut dyn Trait,使用 *mut ()。
vtable 可能类似:
| |
实现本身可能类似:
| |
总之,我们通过将基础类型的引用替换为指向相同数据的适当指针类型来擦除基础类型,既在宽引用(&dyn Trait、&mut dyn Trait)中,也在 vtable 函数指针中。编译器保证没有 ABI 不匹配。
提醒: 这只是帮助高层理解和讨论的草图,不一定是实际实现方式。
另一篇相关博客文章。 写于 2015 年,Rust 已有变化。例如,trait 对象过去在类型位置只写 Trait 而非 dyn Trait。需根据上下文判断他们说的是 trait 还是 dyn Trait 类型。
其他接收者
再看一个函数签名:
| |
这如何工作?内部 Box<BaseType /* : Sized */> 是瘦指针,而 Box<dyn Trait> 是宽指针,与 &mut dyn Trait 类似(尽管 Box 指针表示所有权而非仅独占)。该方法的实现也与 &mut dyn Trait 类似:
| |
简而言之,编译器知道如何从类型擦除形式(如 Box<Self>)转换为与基础类型 ABI 兼容的形式(Box<BaseType>),适用于每个支持的接收者类型。
这是实现细节,但当前编译器通过 DispatchFromDyn trait 知道如何转换。文档列出了当前支持的类型限制(部分仅在不稳定特性 arbitrary_self_types 下可用)。
超 trait 也会实现
我们稍后更详细看超 trait,此处简要说明:有超 trait 时:
trait SuperTrait { /* ... */ }
trait Trait: SuperTrait { /* ... */ }
dyn Trait 的 vtable 包含 SuperTrait 的方法,编译器为 dyn Trait 提供 SuperTrait 的实现,就像提供 Trait 的实现一样。
Box<dyn Trait> 和 &dyn Trait 不会自动实现 Trait
可能令人意外的是,Box<dyn Trait> 和 &dyn Trait 都不会自动实现 Trait。为什么?
简而言之,因为并非总是可能。
如稍后所述,trait 可能有 dyn Trait 无法分派、但必须为任何 Sized 类型实现的方法。例如没有接收者的关联函数:
| |
编译器无法为此类关联函数生成函数体,没有它就无法提供完整的 Trait 实现。
此外,可分发方法的接收者并非总有意义:
| |
&dyn Trait 可以产生 &BaseType,但不能产生 &mut BaseType,因此当唯一已有实现是针对 BaseType 时,无法为 &dyn Trait 实现 Trait::takes_mut。
类似地,Arc<dyn Trait> 无法调用 Box<dyn Trait> 或反之,等等。
自行实现
若 Trait 是本地 trait,可以像为任何其他类型一样为 Box<dyn Trait + '_> 等实现它。但要小心,容易意外写出递归定义!
此外,&T、&mut T 和 Box<T> 是基础(fundamental)类型,在孤儿规则(限制可写的 trait 实现)方面与 T 行为相同。若 Trait 是本地 trait,则 dyn Trait + '_ 是本地类型。
因此你甚至可以为 Box<dyn Trait + '_>(及其他基础包装类型)实现其他 trait!
遗憾的是,Rc、Arc 等不是基础类型,因此不涵盖所有用例。
实现不能被直接覆盖
编译器提供的 Trait 对 dyn Trait 的实现不能被你的代码覆盖。若尝试直接定义自己的实现,会得到编译错误:
| |
此外,若有为实现 Trait 的 blanket 实现,且 dyn Trait 满足该实现的约束,你的实现通常会被忽略;仍使用编译器为 dyn Trait 定义的实现:
| |
更复杂的实现也适用,也适用于 dyn Trait 的超 trait 实现。
我们稍后会看到这很有用。但遗憾的是,编译器实现优先于 blanket 实现存在一些编译器 bug。 如何处理尚未确定;可能禁止某些 blanket 实现,或某些 trait 不再 dyn 兼容,或在部分或大多数情况下用户提供的实现优先于编译器提供的实现。(上述简单模式的一般用法几乎肯定太广泛而无法废弃。且编译器提供的实现必须对 dyn Any 优先。)
实现不能被间接绕过
你可能知道,当具体类型有与 trait 方法同名、同接收者的固有方法时,方法查找时固有方法通常优先:
| |
遗憾的是,dyn Trait 没有此功能。可以写实现,但与上面例子不同,它们会被视为与 trait 方法歧义:
| |
此外,没有像普通 struct 那样专门调用固有方法的语法。即使尝试隐藏 trait,固有方法也无法访问,是死代码。
想法似乎是 trait 方法「就是」dyn Trait 的固有方法,但这很不幸,因为它阻止直接提供如上 non_dyn_dispatchable 覆盖。见 issue 51402。
不试图遮蔽 Trait 方法的 dyn Trait 实现可以工作:
| |
dyn Trait: Trait 的一个小众例外
trait 上的某些约束直到尝试使用该 trait 时才检查,即使 trait 被视为 dyn 兼容。因此有时可以创建不实现 Trait 的 dyn Trait!
| |
对此例,可以提供实现使 dyn Iterable 满足约束。 若不可能,可能需要去掉约束或放弃 trait 的 dyn 兼容性。