4.4 dyn 兼容性(对象安全)
5 分钟阅读
译文 · 基于 Learning Rust
dyn 兼容性(对象安全)
原文链接: https://quinedot.github.io/rust-learning/dyn-safety.html
存在无法创建 dyn Trait 的 trait:
| |
此处不重复所有规则,直接链接参考手册。 请先阅读。(如参考手册所述,dyn 兼容性曾称为对象安全。本指南早期版本使用「dyn 安全」一词。)
截至本文写作时,参考手册尚未更新:可通过添加 where Self: Sized 约束使关联类型和GAT对 trait 对象不可用。暂称此为将 GAT(或关联类型)选择退出「dyn 可用」。
可能不明显的是为何存在这些限制。本页其余部分探讨部分原因。
Sized 约束
在讨论限制之前,先说明 Sized 约束如何与 dyn Trait 和 dyn 兼容性配合。
Rust 用 Sized 表示:
- trait 不是
dyn兼容的 - 关联类型或GAT不是
dyn可用的 - 方法不是
dyn可分发的 - 关联函数对
dyn Trait不可调用- 尽管目前永远不行,为明确和未来兼容仍需声明
这有一定道理,因为 dyn Trait 不是 Sized。因此 dyn Trait 不能实现以 Sized 为超 trait 的 trait,dyn Trait 也不能调用需要 Sized 的方法或关联函数。
然而,这仍是权宜之计,因为存在不是 Sized 但也非 dyn Trait 的类型,我们可能想为它们实现 trait,包括不可 dyn 分发的方法(如泛型方法)。目前在 Rust 中不可能(不可 dyn 分发的方法对其他实现该 trait 的非大小类型也不可用)。
若想阅读关于此权宜之计局限性的吐槽,请展开。
考虑此例,为保持 dyn 兼容而添加 Sized 约束:
| |
若尝试为 str 实现此 trait,即使有 method 在逻辑上合理,也无法使用。可以这样写实现,但不能调用,因为 trait 上的约束优先。
| |
| |
不如直接省略。
| |
情况相当糟糕。理想情况下应有独立 trait 选择退出 dyn 兼容性和可分发性,而非用 Sized;可称 NotDyn。然后可有 Sized: NotDyn 保持兼容,将上面约束改为 NotDyn,str 的实现就能工作。
情况已有改善,未来可能更多:
- 自 Rust 1.87 起, 可简单省略不可调用的方法(之前必须提供方法)
- RFC 2056 也允许用显然不可满足的约束定义方法,无需利用高阶约束技巧(但仍不可调用)
- RFC 3245 或扩展可能允许调用
<str as Trait>::method及更一般的精炼实现
但我认为消除 dyn 兼容性与 Sized 的混淆会更清晰正确,无论未来有何变通。
接收者限制
dyn 可分派方法需要某种基于 Self 的接收者,是为了确保 vtable 可用。需要存在指向 Self 的某种宽指针,才能找到 vtable 并执行动态分发。
可以说可扩展到接受单个非接收者 &Self 等方法。
至于接收者类型的其他限制,编译器必须知道如何从类型擦除版本回到原始版本才能实现 Trait。未来可能泛化,但目前是受限集合。
泛型方法限制
要支持类型泛型方法,vtable 中需要泛型可能采用的每种类型的函数指针。这不仅使 vtable 庞大,还需要某种全局分析。毕竟使用你 trait 的每个 crate 都可能定义满足约束的新类型,他们(或你)也可能想用那些类型调用该方法。
有时可通过擦除问题中的泛型类型参数来绕过(在主方法中、作为替代方法,或在不同的「擦除」trait 中)。我们稍后有一个示例
使用 Self 的限制
以接收者以外形式接受 Self 的方法可能依赖参数与实现类型完全相同。基础类型擦除后无法依赖这一点。
| |
若为 dyn PartialEq 实现,rhs 参数会像 self 一样是 &dyn PartialEq。但没有保证基础类型相同!u8 和 String 都实现 PartialEq,但没有设施比较它们是否相等(Rust 也不打算任意处理)。
有时可为其他 dyn Trait 提供自己的实现,可能利用 Any trait 模拟动态类型和反射。我们稍后给出此方法的示例
impl Clone for Box<dyn Trait> 示例 演示了 Self 作为返回值的情况。
GAT 限制
GAT 太新,尚不支持类型擦除。需要某种方式将 GAT 嵌入 dyn Trait 作为参数,类似非泛型关联类型的做法。
自 Rust 1.72 起, 可通过添加 Self: Sized 约束选择退出 GAT 的 dyn 可用性,从而不必将 GAT 命名为参数。
这与非泛型关联类型的相同能力类似。有趣的是,不仅可以指定具体 GAT 相等……
| |
……还有高阶 GAT 相等:
| |
| |
但与非泛型关联类型情况一样,任何对等式的使用都必须间接完成,因为 dyn Trait 本身不能在自己的实现中定义 GAT。
关联常量限制
类似地,支持关联常量至少需要关联常量相等支持。
返回位置 impl Trait 限制
使用RPIT 的 trait 方法,概念上是声明不透明关联类型或 GAT 的语法糖。此外,即使 RPIT 不捕获泛型参数因而对应返回关联类型,目前也无法命名该关联类型。
类似泛型方法,有时可通过擦除返回类型绕过。(注意有权衡,但讨论更适合专门的 RPIT 指南。)
历史
对象安全在 RFC 0255 中引入, RFC 0546 移除了 trait 上的隐式 Sized 约束,并添加有(显式)Sized 约束的 trait 不是对象安全的规则。
两个 RFC 都在 Rust 1.0 之前实现。