4.3 dyn Trait 强制转换
7 分钟阅读
译文 · 基于 Learning Rust
dyn Trait 强制转换
原文链接: https://quinedot.github.io/rust-learning/dyn-trait-coercions.html
一些典型的(就强制转换内容而言)dyn Trait 强制转换如下:
| |
实践中通常比这些更简洁,因为我包含了一些通常隐含或不使用的显式生命周期和约束。例如泛型类型参数上的 Sized 约束通常隐含,但我显式写出以强调我们讨论的是 Sized 基础类型。
要点是:给定 dyn 兼容的 Trait,且当 T: 'a + Trait + Sized 时,可将 Ptr<T> 强制转换为 Ptr<dyn Trait + 'a>,其中 Ptr 为 &_、Box<_> 等支持的指针类型。
若需要 dyn Trait + Send + 'a,自然还需要 T: Send,其他自动 trait 同理。
本节其余部分探讨这些典型例子之外的情况,以及强制转换的一些限制。
关联类型
当 trait 有一个或多个非泛型关联类型时,每个具体实现者为每个关联类型选择单一、静态已知的类型。对基础类型,关联类型是实现类型和 trait 的「输出」:知道后两者即可静态确定关联类型。
那么 Trait 对 dyn Trait 的实现中关联类型应该是什么?
没有单一答案;它们需要随擦除的基础类型而变化。
然而,带关联类型的 trait 的 dyn Trait 太有用了,不能因此使带关联类型的 trait 无法用于 dyn Trait。相反,trait 中的关联类型实质上成为 dyn Trait 类型构造器的命名类型参数。(回想它已因 trait 对象生命周期而是类型构造器。)
给定
| |
我们有
| |
等等。dyn Trait<...> 中的关联类型必须解析为具体类型,dyn Trait<...> 才是具体类型。
自然,只有既实现 Iterator 又在实现中 type Item = String 时,才能强制转换为 dyn Iterator<Item = String>。语法与关联类型 trait 约束相同:
| |
参数被命名有多方面好处。通常很相关,例如 Iterator 返回什么 Item(尤其关联类型命名良好时)。也无需以良好定义的方式排序关联类型,如字典序或声明顺序(后者太脆弱)。
命名参数必须在所有有序参数之后指定。
| |
选择退出 dyn 可用性
自 Rust 1.72 起, 若对关联类型添加 where Self: Sized 约束,则视为非 dyn 可用。关联类型对 dyn Trait 不可用,也不再需要用命名参数约束。
| |
虽然会产生警告,仍可可选指定关联类型,尽管 dyn Trait 本身不可用。注意这会导致不兼容类型并限制可能的强制转换:
| |
这为为 Box<dyn Trait> 实现 trait 带来一些有趣可能:
| |
警告目前说「可以指定关联类型但不能使用」,但此例表明技术上不成立。我认为这类用法未被预料。
以这种方式指定非 dyn 可用关联类型不是错误,是因为曾有一段时间可以为关联类型添加 Self: Sized 约束,但仍必须在 dyn Trait<..> 中命名关联类型。因此将警告改为错误会是破坏性变更。
鉴于潜在用处,我认为警告至少应改写,或许应重命名。
无嵌套强制转换
unsizing 强制转换需要在间接层之后发生(如引用或 Box 中),以容纳擦除类型 vtable 的宽指针(且不支持移动非大小类型)。
然而,unsizing 强制转换通常只能在单层间接之后发生。例如不能将 Vec<Box<T>> 强制转换为 Vec<Box<dyn Trait>>。为什么?Box<T> 和 Box<dyn Trait> 布局不同!前者是一个指针大小,后者是两个指针。整个 Vec 需要重新分配以适应这种变化:
| |
一般而言,unsizing 强制转换消耗原指针(引用、Box 等)并产生新指针,这在嵌套上下文中无法发生。
内部哪些强制转换可能由 CoerceUnsized trait 和(编译器实现的)Unsize trait 决定,见文档。
例外情况
有些实质性和表面性例外,unsizing 强制转换可在嵌套上下文中发生。
若跟随上面链接,会看到某些类型如 Cell 以递归方式实现 CoerceUnsized。想法是 Cell 等与其泛型类型参数布局相同。因此外层 Cell 不算「嵌套」。
| |
我们将在 upcoming 章节涵盖其他表面例外(实际是超类型强制转换)。
Sized 限制
基础类型必须满足 Sized 约束才能强制转换为 dyn Trait。例如 &str 不能强制转换为 &dyn Display,尽管 str 实现 Display,因为 str 是非大小的。
为何有此限制?&str 也是宽指针;由指向 UTF-8 字节的指针和字节数的 usize 组成。类似地切片引用 &[T] 是指向连续数据的指针和元素个数。
从 &str 或 &[T] 创建的 &dyn Trait 天真地需要「超宽指针」:数据指针、元素计数和 vtable 指针。但 &dyn Trait 是静态布局的具体类型——两个指针——因此天真方法行不通。此外,若要强制转换超宽指针呢?每次递归强制转换都需要另一个指针,大小无界。
非天真方法需要为擦除的非 Sized 基础类型的动态分发特殊处理。例如类型擦除 str 后,已丢失 &str 也是宽指针及如何创建该宽指针的信息,但代码需要重建宽指针才能动态分发。
因此若要非天真支持非大小类型,dyn Trait 需要在运行时检查如何构造指向擦除基础类型的指针:瘦指针一种可能,每种支持的宽指针类型各一种可能。不仅如此,所需元数据(如 str 长度)必须存储在某处,不能像 vtable 那样在静态内存中。
因此 simply 不支持非大小基础类型。
有时可通过变通,例如为 &str 而非 str 实现 trait,然后将 &'_ str 强制转换为 dyn Trait + '_(因为引用总是 Sized)。
| |
Sized 也用作某种「非 dyn」标记,我们稍后探讨。
Sized 限制有一个广泛例外:在 dyn Trait 形式之间强制转换,见下面小节。
丢弃自动 trait
可以将 dyn Trait + Send 强制转换为 dyn Trait,类似地可丢弃任何其他自动 trait。
尽管dyn Trait 不是 dyn Trait + Send 的超类型, 这仍称为将 dyn Trait + Send 向上转型为 dyn Trait。
自动 trait 没有方法,因此这些强制转换不需要改变 vtable。它们允许从更受限的函数(如需要 dyn Trait + Send)调用限制更少的函数(接受 dyn Trait)。强制转换是必要的,因为再次强调,这些是(不同的)具体类型,不是泛型、子类型或动态类型。
虽然不需要改变 vtable,此强制转换仍不能在嵌套上下文中发生。
丢弃主 trait
自 Rust 1.84 起, 也可丢弃主 trait 创建类似 dyn Send 的类型,若出于某种原因需要。可通过改变指针实现,也可保持指针不变;此类强制转换后唯一能动态分派的方法是调用析构函数。
自反情况
可以将 dyn Trait 强制转换为 dyn Trait。
抱歉,我们又不够精确。可以将 dyn Trait + 'a 强制转换为 dyn Trait + 'b,其中 'a: 'b。这对dyn Trait + '_ 的借用如何工作很重要。
生命周期在编译时擦除,vtable 与生命周期无关。尽管如此,此 unsizing 强制转换仍不能在嵌套上下文中发生。
然而,在后续关于型变的章节 我们将看到,在协变上下文中,方差甚至允许在嵌套上下文中缩短 trait 对象生命周期。之后关于高阶类型的章节 探讨另一种可视为自反的生命周期相关强制转换。
超 trait 向上转型
自 Rust 1.86 起 也可将(「向上转型」)dyn SubTrait 强制转换为 dyn SuperTrait。
再次明确,这是强制转换而非实际的子/超类型关系,尽管术语如此。虽是实现细节,转换可能涉及替换 vtable 指针(与丢弃自动 trait 或自反情况对比)。
你也可以编写自己的「手动」超 trait 向上转型(在 trait 向上转型稳定化之前更重要)。
仅 dyn 兼容的 trait
trait 上还有其他限制未在此讨论,例如(尚)不支持带 GAT 的 trait。我们在下一节涵盖。