深入理解特征对象之三:强制类型转换

本文是 rust 社区大神 quinedot 的系列文章 的翻译版。我在国内各个平台似乎都没有看到现存的翻译,因此尝试做些搬运工作。由于本人的技术和翻译水平都非常有限,难免有错误,非常欢迎大家对不当之处进行指正。

这是本系列文章的第 3 篇。

1. 前言

在 rust 中,我们通常使用强制类型转换(coercion)来将一些类型转换为特征对象,下面是用函数形式写出的一些典型场景:

fn coerce_ref<'a, T: Trait + Sized + 'a>(t:    &T ) -> &(  dyn Trait + 'a) { t }
fn coerce_box<'a, T: Trait + Sized + 'a>(t: Box<T>) -> Box<dyn Trait + 'a> { t }
fn coerce_arc<'a, T: Trait + Sized + 'a>(t: Arc<T>) -> Arc<dyn Trait + 'a> { t }

在实践中,我们看到的代码往往不会这样繁杂,因为这里引入了一些显式生命周期和特征约束,而通常这些约束都是隐含的或者无需使用。例如,泛型类型参数上的 Sized 约束一般是隐含的,但上面的代码里我们显式地标出,强调正在讨论的 base type 都是定长类型(Sized)。

假设特征 Trait 是对象安全(object safe)的,并且满足 T: 'a + Trait + Sized,那么对于一些受支持的指针类型 Ptr(如 &_Box<_>),我们就可以将 Ptr<T> 强制转换成 Ptr<dyn Trait + 'a>

如果我们想要一个 dyn Trait + Send + 'a,自然我们也需要 T: Send,对于任何其他自动特征(auto trait)也是如此。

在本文的其余部分,我们将研究这些典型案例之外的情况,以及强制转换的一些使用限制。

2. 关联类型(associate type)

当一个特征拥有一或多个非泛型的关联类型时,该特征的每个具体实现类型,都会为各关联类型选择一个唯一确定的、静态可知的类型。也就是说,我们可以把关联类型理解成某个 base type 加其实现的特征的 “输出”。

那么,为 dyn Trait 实现 Trait 时,应该如何确定含有的关联类型呢?这个问题没有唯一确定的答案,实际上,在这种情况下,关联类型会随着被擦除的 base type 变化。

这样一来,矛盾就出现了:这与我们最开始提到的“特征的实现者应该对每个关联类型唯一确定”的原则是相违背的。对于这个问题,一个简单粗暴的解决方法是:一旦特征 Trait 含有关联类型,那我们就索性禁止构建 dyn Trait 的特征对象。然而倘若真的引入这种规则,就会让特征对象的实用性大打折扣,rust 的设计者们无法接受这种做法。最终,rust 团队采用的方案是,让特征中的关联类型本质上成为 dyn Trait 类型构造器的具名参数(named parameter)。(回想一下本系列文章的第一篇,由于特征对象的生命周期,dyn Trait 本质上是一个类型构造器,dyn Trait + 'a 才是一个具体类型,只是 'a 往往被隐藏。)

例如,如果给出

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

那么可以有:

dyn Iterator<Item = String> + '_
dyn Iterator<Item = i32> + '_
dyn Iterator<Item = f64> + '_

等等。在 dyn Trait<...> 中的关联类型必须解析为具体类型,以便 dyn Trait<...> 成为一个具体类型。

如果你在 Iterator 的实现中指定了 type Item = String,那么很自然地,你就只能将类型强制转换为 dyn Iterator<Item = String> 了。下面的写法反映了关联类型特征约束的样式:

fn takes_string_iter<Iter>(i: Iter)
where
    Iter: Iterator<Item = String>,
{
   // ...
}

使用具名参数有许多好处。首先,一个类型和它拥有的具名参数往往具有很强的逻辑关联,例如具名参数能够清晰地表示一个 Iterator 返回的 Item 是什么(当然,这些关联类型需要命名得当)。其次,它还避免了在泛型列表中对多个关联类型进行排序,例如按字典序排列或是按声明顺序,尤其是后者,很容易出差错。

译注:如果不是具名参数,那么可能会出现这种情况:

trait Trait {
    type Assoc1;
    type Assoc2;
    type Assoc3;
}

// 如果采用下面这种语法,一旦关联类型数量较多,读者就很难立刻将 i32 String &[u8] 与 Assoc1/2/3 对号入座
let d: Box = ...; // 并非真实语法
// 而如果采用具名参数的写法,就清晰得多:
let d: Box = ...;

然而,这些具名参数必须在普通泛型类型参数(而它们是需要严格按顺序排列的)之后指定。

trait AssocAndParams<T, U> { type Assoc1; type Assoc2; }
// 特性的顺序类型参数必须按声明顺序(这里是 String 然后是 usize)。
// 之后是具名的关联类型参数,这些参数可以任意方式排序。
fn foo(d: Box<dyn AssocAndParams<String, usize, Assoc1 = i32, Assoc2 = u32>>)
->
    Box<dyn AssocAndParams<String, usize, Assoc2 = u32, Assoc1 = i32>>
{
   d
}

2.1. 让关联类型失去 dyn 可用性

从 Rust 1.72 版本开始,如果我们在关联类型中添加了 where Self: Sized 约束,则该关联类型便不再满足对象安全(Object safe),在本系列文章中我们称之为“不可 dyn”(dyn-unusable TODO:)。在这种情况下,虽然我们还是能够为其 base type 构造特征对象,但是在构造时不再需要为关联类型指定具名参数,且在特征对象中也不再能够使用这个关联类型。

trait Trait {
    type Foo where Self: Sized;
    fn foo(&self) -> Self::Foo where Self: Sized;
    fn bar(&self) {}
}

impl Trait for i32 {
    type Foo = ();
    fn foo(&self) -> Self::Foo {}
}

impl Trait for u64 {
    type Foo = f32;
    fn foo(&self) -> Self::Foo { 0.0 }
}

// 不再需要 `dyn Trait<Foo = ()>`!
let mut a: &dyn Trait = &0_i32;
//  让特征对象指向另一 base type 时,无需让关联类型保持一致
a = &0_u64;
// 这会失败,因为类型未定义 (`dyn Trait` 不可 `Sized`)
// let _: <dyn Trait as Trait>::Foo = todo!();

在这种情况下,我们还是可以用具名参数的方式指定关联类型,不过会报 warning,而且即使这么做了 dyn Trait 也还是无法使用这个关联类型。请看编译器的 warning:

warning: unnecessary associated type bound for not object safe associated type
  --> src/main.rs:24:23
   |
24 | let mut a: &dyn Trait<Foo = ()> = &0_i32;
   |                                   ^^^^^^^^ help: remove this bound
   |
   = note: this associated type has a `where Self: Sized` bound. Thus, while the associated type can be specified, it cannot be used in any way, because trait objects are not `Sized`.
   = note: `#[warn(unused_associated_type_bounds)]` on by default

事实上,这样做不仅没能改变 dyn Trait 无法使用该关联类型的现状,还会导致对其他类型的不兼容,从而进一步限制强制类型转换的可用性:

let mut a: &dyn Trait<Foo = ()> = &0_i32;
// 下面会导致编译不通过
a = &0_u64;

不过这也引入了一些有趣的可能性,例如为 Box<dyn Trait> 实现上面的 Trait 时(TODO:),下面的写法是可以通过编译的:

impl<T: Default> Trait for Box<dyn Trait<Foo = T>> {
    type Foo = T;
    fn foo(&self) -> Self::Foo {
        T::default()
    }
}

目前的 warning 写的是 "虽然可以指定关联类型,但无法以任何方式使用",但这个例子表明技术上并非如此。我认为这种用法只是没有被预料到。

对于这种场景,编译器不报 error 而是报 warning 的原因是:曾经有一段时间, rust 允许用户为关联类型添加 Self: Sized 约束,但仍然需要在 dyn Trait<..> 中显式命名关联类型。因此,将 warning 变成 error 会是一次破坏性变更。

鉴于其潜在的实用性,我认为至少应该修改这个 warning 的措辞,甚至考虑重新命名。

3. 嵌套的强制类型转换是不可行的

对非定长类型(unsized)的强制类型转换需要经过一个间接层(例如引用或 Box)才可行,这是因为需要容纳指向虚表的宽指针,而且 rust 也不允许 move 一个非定长类型。

然而,如果有多个间接层嵌套,那么编译器就不能执行这种强制类型转换了。例如,我们不能将 Vec<Box<T>> 强制转换为 Vec<Box<dyn Trait>>。这是因为 Box<T>Box<dyn Trait> 有不同的内存布局,前者占用的内存空间就是一个指针的大小,而后者则是两个指针的大小。所以,整个 Vec 都需要重新分配内存以适应这样的变化:

fn convert_vec<'a, T: Trait + 'a>(v: Vec<Box<T>>) -> Vec<Box<dyn Trait + 'a>> {
    v.into_iter().map(|bx| bx as _).collect()
}

通常情况下,非定长类型的强制类型转换会消耗(consume)掉原始的指针(引用、Box 等),并产生一个新的指针,而这在多层嵌套的场景下是无法发生的。

从编译器内部视角来说,哪些强制类型转换是可能的取决于 CoerceUnsized 特征,以及由编译器实现的 Unsize 特征,正如链接中的文档里讨论的那样。

3.1. 一些例外

事实上,有一些例外情况:对于一些特别的间接层,即使发生了嵌套,也依然可以进行非定长类型的强制类型转换。

如果你阅读了上面链接中的文章,会发现有些类型,比如 Cell,以递归的方式实现了 CoerceUnsized。这是因为这些类型(包括 CellRefCellSyncUnsafeCellUnsafeCell)与它们的泛型参数有相同的内存布局。因此,在外侧再包上一层 Cell 不会被视为是“嵌套”的。

// 失败 :-(
//fn coerce_vec<'a, T: Trait + 'a>(v: Vec<Box>) -> Vec<Box<dyn Trait + 'a>> {
// v
//}
// 成功!:-)
fn coerce_cell<'a, T: Trait + 'a>(c: Cell<Box>) -> Cell<Box<dyn Trait + 'a>> {
c
}

我们将在接下来的章节(TODO:)讲解那些表面上的例外情况(其实只是 supertype 强制转换)。

4. Sized 的限制

任何一个 base type ,必须是定长类型(Sized),才能被强制类型转换为 dyn Trait。例如,尽管 str 实现了 Display,但由于它是非定长类型(unsized), &str 便不能被强制转换为 &dyn Display

为什么会有这个限制呢?事实上,&str 本身也是一个宽指针,它包含一个指向 UTF8 字节的指针,和一个 usize,表示字节的数量。类似地,切片引用 &[T] 包含一个指向连续数据的指针,和一个表示元素数量的计数。

由此观之,如果要从 &str&[T] 创建特征对象,那么这个特征对象的内存布局似乎需要变成一个含有三个成员的“超宽指针”:一个指向数据的指针、一个元素计数和一个虚表指针。但对于编译器而言,&dyn Trait 是一种具体的类型,因此它有着确定的结构,只包含两个指针。所以,“超宽指针”这种 naive 的设想是不可行的。而且,这种设想还有另一个问题:如果我们想对一个“超宽指针”进行强制类型转换,那会发生什么呢?可以想象,每次进行递归的转换时都会引入一个新的指针,使得其内存变得无限大。

一种不那么 naive 的方法是在动态分发机制上做改动,对这些 Unsized 的 base type 做专门的处理。举个例子,在类型转换过程中,一旦擦除了 str 类型,我们就丢失了一些信息:例如 &str 也是一种宽指针,以及创建这种宽指针的方法。然而,编译器又需要重新创建一个宽指针,才能进行动态分发。

因此,如果要让编译器支持构建 Unsized 的特征对象,那么在构建指向 base type 的指针时,事情就会变得很复杂。首先,我们需要为普通的窄指针设计一种处理逻辑;其次,对于想要支持的每种宽指针类型,我们都要额外引入一段专门的处理逻辑。不止如此,关键的元数据(比如 str 的长度)也必须找地方存放起来,而且还不能像虚表那样放在内存里的 .data 段(静态内存)中。

这将会是地狱般的复杂,因此 rust 的设计者们决定索性不支持 Unsized 的 base type 了。

不过有时候,你可以通过例如为 &str 而不是 str 实现特征,然后将 &'_ str 强制类型转换为 dyn Trait + '_ 来绕过这个限制,因为引用是一种定长类型。

// 这段代码不能通过编译,因为 str 是非定长类型,
// 所以我们不能将 str 强制转换为 dyn Display,
// 因而无法将 &str 强制转换为 &dyn Display。
// let _: &dyn Display = "hi";

// 然而,&str 也实现了 Display。(如果有 T: Display,那么 &T: Display)。
// 由于 &str 是定长类型(Sized),我们就能将 &&str 强制转换为 &dyn Display:
let _: &dyn Display = &"hi";

定长类型(Sized)也被用作某种“非-dyn”的标识,这一点我们稍后(TODO:)会进一步探讨。

dyn Trait 自身的不同形式间进行强制类型转换时,会出现一种常见的异常,而它正是与 Size 限制有关。我们会在下文讨论这个情况。

5. 删除自动特征(auto trait)

我们可以将 dyn Trait + Send 强制转换为 dyn Trait,并以类似的方式丢弃任何其他自动特征。

虽然 dyn Trait 不是 dyn Trait + Send 的超类型(super type),但这仍然被称为将 dyn Trait + Send 向上转换(upcasting)为 dyn Trait

请注意,自动特征不拥有方法,因此这些强制类型转换无需对虚表进行任何更改。这样一来,rust 允许用于从一个较严格的函数(例如要求 dyn Trait + Send 的函数)调用一个限制较少的函数(接受 dyn Trait 的函数)。这种强制类型转换是必要的,因为它们是不同的具体类型,而不是泛型、子类型或动态类型。

不过,即使不需要修改虚表,这种强制类型转换依然不可用于嵌套场景。

6. 反身转换(reflexive)

下面我要说一条看起来是废话的规则:你可以将 dyn Trait 强制类型转换为 dyn Trait

实际上它的本意是:你可以将 dyn Trait + 'a 强制转换为 dyn Trait + 'b,其中 'a: 'b。这对于 dyn Trait + '_ 的借用机制起到了重要作用。

在编译过程中,生命周期会被擦除,因此虚表在任何生命周期下都是相同的。尽管如此,这种 Unsized 的强制类型转换依然不可用于嵌套场景。

然而在未来的章节(TODO:)中,我们将看到,即使在嵌套的情况下,协变(variance)也可以缩短特征对象的生命周期,前提是该嵌套也是协变的。紧接着的关于高阶类型的章节(TODO:)将探讨另一种与生命周期相关的强制转换,也可以被视为是反身的。

7. 超类型的 upcast

虽然目前在 rust 稳定版中还不支持,但从 dyn SubTraitdyn SuperTrait 进行向上转换(upcast)的功能预计在未来某一天会实现。

再次强调,这明确是一种强制类型转换,而非子类型与超类型的关系(尽管术语如此)。尽管这是一个具体的实现细节,但与前几个示例相反,这种转换可能会涉及替换虚表指针。

在该功能尚未稳定之前,你可以手动实现超类型向上转换逻辑。

8. 仅适用于对象安全的特征

在使用特征对象时,对于特征还有其他限制,我们在这里尚未讨论,比如目前还不支持具有关联泛型类型(GATs, Generic Associated Types)的特征。我们在下一节(TODO:)中会详细介绍这些内容。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇