深入理解特征对象之一:特征对象综述

1. 译者注

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

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

一些比较难以定夺的翻译:

base type

原作者在文中大量使用该词,其本意是指实现了某个特征的具体类型,原文中能看到的另一种同义表达是 "implementor",即“实现者”。例如对于一个特征 Trait ,如果有 impl Trait for Foo,那么 Foo 就是一个 base type 或 implementor。如果翻译成“基类”,容易让人联想到 C++ 等经典 OOP 语言中的 base class,从而导致混淆。至少有三个原因让我拒绝这种可能造成混淆的翻译:

  • base type 是类型(type)而不是类(class)
  • rust 的特征系统与 OOP 的继承概念存在一定差距
  • Trait 是抽象,base type 是具体实现;而 C++ 中基类是抽象、派生类是实现,二者的逻辑恰好相反

而用字面翻译“基础类型”也略显拗口。因此,我暂时决定不翻译这个词。

2. 特征对象(dyn Trait)是什么

dyn Trait 也被称为特征对象(trait object),是一种编译器提供的类型,它实现了特征 Trait

在 rust 中,实现了 Sized 的类型被称为定长类型,这表示该类型的大小在编译时是已知的。任何实现了 Trait 的定长类型都可以被强制转换(coerce)为 dyn Trait 特征对象类型,在此过程中其原本的类型被“擦除”了。由于 Trait 的不同实现可能具有不同的大小,我们无法在编译期得知一个特征对象的静态大小,因此它没有实现 Sized。对于这样的类型,我们称之为“不定长类型(unsized)”或“动态大小类型(dynamically sized types, DST)”。

每个特征对象都是通过对某个现有变量进行类型擦除得到的。这意味着我们不能只定义一个特征 Trait 而后立刻凭空创建一个 dyn Trait 特征对象;而是必须再写一个实现了 Trait 的 base type,然后对该类型进行强制转换。

目前,Rust 对不定长类型存在诸多限制:编译器不允许将不定长类型作为函数的参数或返回值,也不允许局部变量使用不定长类型。因此,我们通常采取某些间接方式(indirection)来使用特征对象,如 Box<dyn Trait>&dyn TraitArc<dyn Trait> 等等。

实际上,还有另一个原因使得我们必须使用这些间接方式:它们要么本质上就是宽指针(wide pointer, or fat pointer),要么含有一个宽指针。宽指针由两个指针构成,其中一个指向原始类型的值,另一个则指向虚表(vtable)。虚表含有许多数据,包括原始类型的真实大小、指向析构函数的指针、指向 Trait 方法的指针,等等。虚表也正是动态分发机制(dynamic dispatchTODO:)的实现原理,让不同特征对象的方法调用可以落到各自类型的实现上。(译注:关于宽指针,可以进一步参考这篇文章

我们还可以构造如 dyn Trait + Send + Sync 这样的特征对象。SendSync 是自动特征(auto-traits),一个特征对象可以包含任意数量的自动特征作为额外的约束。每个不同的 Trait + 自动特征 的组合都被认为是一种独立的类型。

不过,rust 只允许一个特征对象包含一个自动特征,所以下面的代码是不能通过编译的:

trait Trait1 {}
trait Trait2 {};
struct S(Box<dyn Trait1 + Trait2>);

当然,我们可以利用特征约束,通过创建一个 supertrait 来解决这个问题,即:

trait Trait1 {}
trait Trait2 {}
trait Super: Trait1 + Trait2 {}
struct S(Box<dyn Super>);

2.1. 特征对象的生命周期

前面我们提到特征对象是一种类型,坦白来讲,这个说法不够准确。dyn Trait 实际上是构造类型的工具(译注:原文是 type constructor,个人拙见这里不应该翻译成“构造函数”),它需要以生命周期作为参数,在这一点上与引用类似。所以,dyn Trait 本身并不是一种类型,带有某个具体生命周期 'adyn Trait + 'a 才是一种真正的类型。

生命周期通常可以省略,这一点我们稍后(TODO:)会探讨,但它始终是类型的一部分,就像生命周期是每种引用类型的一部分一样,即使在省略时也是如此。

2.2. 关于关联类型

如果一个特征具有非泛型的关联类型,那么在构建特征对象时,需要显式指定这些关联类型:

let _: Box<dyn Iterator<Item = i32>> = Box::new([1, 2, 3].into_iter());

我们将在后面的部分(TODO:)进一步探讨这个话题。

3. 特征对象不具有的性质

3.1. 不实现 Sized

我们前面已经提到过,特征对象是不定长类型,没有实现 Sized。值得一提的是,在泛型中,泛型参数是默认隐含了 Sized 约束的。因此,在泛型中使用 dyn Trait 时,你可能需要通过使用 ?Sized 来移除这个隐藏的约束。请看下面的代码:

// 函数 `foo` 只接受 `T: Sized`,它不接受 `&dyn Trait`,
// 因为 `dyn Trait` 没有实现 `Sized`。
fn foo<T: Trait>(_: &T) {}

// 函数 `bar` 接受任何 `T: Trait`,即使没实现 `Sized`。
fn bar<T: Trait + ?Sized>(_t: &T) {
    foo(t); // 这行代码会报错,这能够证明 `foo` 不能接受非 `Sized` 类型
}

3.2. 特征对象既不是泛型,也不是动态类型

有一点容易混淆:对于一个具体的生命周期 'adyn Trait + 'a 是一个在编译时已知的静态类型。但是,在强制转换过程中被擦除的 base type 并不是静态已知的。

例如,我们来看看这两个函数:

fn generic<T: Trait>(_rt: &T) {}
fn not_generic(_dt: &dyn Trait) {}

先来看第一行的泛型函数 generic()。对于传递给函数的每种类型 T,编译器都会生成一个对应版本的函数,这个过程被称为单态化(monomorphization)。(另外,生命周期在编译过程中会被擦除,不会单态化。)通过指定类型,我们甚至可以获取到对应单态化版本的函数指针:

let fp = generic::<String>; // fp 的类型是 fn generic<String>(&String)

也就是说,泛型函数 generic() 的实际类型是需要包含一个特定的 T: Trait 作为参数的。

相比之下,采用特征对象的 not_generic() 函数只会有一种编译后的版本。在传参时,编译器会将实现了 Trait 的 base type 擦除为 dyn Trait + '_。可见,函数 not_generic 的类型不是由泛型类型参数化的。

再来看看这个函数:

fn generic<T: Trait>(bx: Box<T>) {}

这里的 bx: Box<T> 不是 Box<dyn Trait>Box<T>只是一个“窄指针”(thin pointer,与宽指针相对),指向堆上的某个特定 T 类型对象。在 1.1 节,我们提到“任何实现了 Trait 的定长类型都可以被强制转换为特征对象类型”;而这里的泛型 T 隐含了 Sized 约束,满足了上述条件,因此我们可以将 bx 强制转换为一个 Box<dyn Trait + '_>。不过,这个过程会导致 bx 彻底变成另一种 Box ,指向的对象变成了一个宽指针,擦除了 T 并引入了虚表指针。

我们将在后面的部分(TODO:)探索更多关于泛型和特征对象的互动细节。

读者们可能会好奇,为什么我们通常都能在 &dyn TraitBox<dyn Trait> 上使用 Trait 里定义的方法,即使没有显式地声明任何此类约束。其原因类似于我们可以在没有具体声明约束的情况下,在 String 上使用 Display 方法:这个类型是静态可知的,编译器认识到 dyn Trait 实现了 Trait,就像它认识到 String 实现了 Display 一样。Trait 的约束只适用于泛型,而不是具体类型。

实际上,Box<dyn Trait> 并非自动实现了 Trait,只是编译器通常使用解引用(deref)的类型转换来处理这种情况。此外,对于许多 std 里的特征,标准库已经帮我们为 Box<dyn Trait> 实现好了。我们也会在后面的章节(TODO:)更详细地谈谈这种实现方式。

如果 Trait 是一个本地特征(local trait,指在我们自己的 crate 里定义的特征),我们便可以在 dyn Trait 上实现方法,甚至为 dyn Trait 实现其他特征,后面会给出一些示例(TODO:)。

3.3. 特征对象并不是一种超类型(supertype)

人们常常会有一种错误的认识:dyn Trait 是所有实现了 Trait 的 base type 的某种超类型,因为我们能够将它们强制转换成 dyn Trait。而特征约束和生命周期约束又恰好共用同一种语法(冒号),这就会导致上述错误认知更加深入人心。

实际上,这种从 base type 到特征对象的强制转换是一种不定长强制转换(unsizing coercion),并非从子类型到超类型的转换;这种强制转换发生在代码中静态已知的位置,并且可能会改变涉及类型的内存布局,例如将窄指针变为宽指针。

此外,我们也不该认为特征 Trait 完全等价于 C++ 等语言中的类(class)。我们不能在没有 base type 的情况下创建特征对象(它们缺少内置的构造函数),且一个特定类型可以实现许多特征。由于可能造成误解,我建议不要将 base type 称为特征的“实例”(instance)。它只是实现了该特征的一个类型,这个类型独立于特征存在。例如,当我们创建一个 String 时,我们创建的 String,而不是“Display(以及 DebugWriteToString 等等)的一个实例”。

当我读到 “Trait 的一个实例”时,我会认为那个变量是某种形式的特征对象,而非某个未被擦除的、实现了 Trait 的 base type。

dyn Trait 实现的方法或特征只局限于特征对象类型本身,并不会对所有其他的 T: Trait 都实现。同样地,为 dyn Trait + Send 实现的方法或特征也不会影响到 dyn Trait ,反之亦然。它们都是单独的、不同的类型。

后面(TODO:)我们将探讨在 Rust 中模拟动态类型(dynamic typing)的方法。我们还将探讨 supertrait 的角色(尽管名称如此,但它仍然不涉及子/超类型的关系)。

在大多数情况下,我们在 Rust 中讨论子类型(subtype)时,唯一相关的是那些拥有更广生命周期的类型。(译注:例如对于 &'a T&'b T ,如果有 'a: 'b ,那么可以认为 &'b T&'a T 的一个子类型)

(苛刻的自我纠正:由于特征对象具有生命周期,因此在生命周期的讨论上它们其实可以是超类型。然而,这并非大多数 Rust 学习者搞混的概念;实现了特征的具体类型与超类型没有关系)

3.4. dyn Trait 也有限制条件

我们将在接下来的章节中详细讨论这一点,但简而言之,并非在任何情况下都能将实现了 Trait 的 base type 强制转换成 dyn Trait。特征和其实现者都必须满足某些条件。

4. 总结

特征对象 dyn Trait + 'a

  • 是一个具体的、静态已知的类型
  • 是需要通过擦除某个 Trait 实现者的具体类型来创建
  • 是通过指向擦除类型值+指向静态虚表的宽指针来使用
  • 是拥有动态大小(unsized,不实现 Sized 特征)
  • 是通过动态分发机制来实现 Trait
  • 不是所有实现了 Trait 的 base type 的超类型
  • 不是动态类型
  • 不是泛型
  • 不可用于所有实现了 Trait 的 base type
  • 不可用于所有特征
知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
暂无评论

发送评论 编辑评论


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