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 Trait
、Arc<dyn Trait>
等等。
实际上,还有另一个原因使得我们必须使用这些间接方式:它们要么本质上就是宽指针(wide pointer, or fat pointer),要么含有一个宽指针。宽指针由两个指针构成,其中一个指向原始类型的值,另一个则指向虚表(vtable)。虚表含有许多数据,包括原始类型的真实大小、指向析构函数的指针、指向 Trait
方法的指针,等等。虚表也正是动态分发机制(dynamic dispatchTODO:)的实现原理,让不同特征对象的方法调用可以落到各自类型的实现上。(译注:关于宽指针,可以进一步参考这篇文章)
我们还可以构造如 dyn Trait + Send + Sync
这样的特征对象。Send
和 Sync
是自动特征(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
本身并不是一种类型,带有某个具体生命周期 'a
的 dyn 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. 特征对象既不是泛型,也不是动态类型
有一点容易混淆:对于一个具体的生命周期 'a
,dyn 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 Trait
或 Box<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
(以及 Debug
、Write
、ToString
等等)的一个实例”。
当我读到 “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 - 不可用于所有特征