本文是 rust 社区大神 quinedot 的系列文章 的翻译版。我在国内各个平台似乎都没有看到现存的翻译,因此尝试做些搬运工作。由于本人的技术和翻译水平都非常有限,难免有错误,非常欢迎大家对不当之处进行指正。
这是本系列文章的第 2 篇。
1. 前言
dyn Trait
作为所有实现了 Trait
的 base type 的抽象,其自身当然也需要实现 Trait
。编译器总是自动生成了这个实现。我们接下来要探讨这种实现的工作原理,以及带来的一些限制。
此外,我们还会讲到一些让人吃惊的 edge case,其中也有一部分与 dyn Trait
实现 Trait
的方式相关。
2. dyn Trait
如何实现 Trait
首先需要声明的是,下面的代码是为 dyn Trait
实现特征 Trait
的一种可能的实现,这只是一份粗略的概述,而非真实 rust 编译器中的写法。rust 编译器的实际处理逻辑包含了很多细节,而那些都是代码实现方面的事情。本文的出发点是,通过展示一种简明的可能实现方式,让读者对特征对象作为一个具体类型有直观的理解,并且认识到特征对象存在一些使用限制的原因。
声明过后,我们先给出一个 Trait
的定义:
trait Trait {
fn look(&self);
fn add(&mut self, s: String) -> i32;
}
接下来我们来看看编译器对这个特征的实现可能会是什么样子。回忆一下前一篇文章的内容,当构建特征对象时,我们需要一个指向擦除的 base type 的指针,以及一个虚表。因此,一种可能的 &dyn Trait
结构是这样的:
#[repr(C)]
struct DynTraitRef<'a> {
_lifetime: PhantomData<&'a ()>,
base_type: *const (),
vtable: &'static DynTraitVtable,
}
// 伪码
type &'a dyn Trait = DynTraitRef<'a>;
这里我们使用了一个窄指针 *const ()
来指向擦除的 base type。类似地,对于 &'a mut dyn Trait
,我们可以略加改动实现一个 DynTraitMut<'a>
结构,而它的 base type 指针应当换成 *mut ()
。
对于上面代码中的虚表 DynTraitVtable
,一种可能的结构是这样的:
#[repr(C)]
struct DynTraitVtable {
fn_drop: fn(*mut ()),
type_size: usize,
type_alignment: usize,
fn_look: fn(*const ()),
fn_add: fn(*mut (), s: String) -> i32,
}
在完成了所有 struct 的声明后,现在该为 dyn Trait
实现 Trait
了,一种可能的写法是:
impl Trait for dyn Trait + '_ {
fn look(&self) {
(self.vtable.fn_look)(self.base_type)
}
fn add(&mut self, s: String) -> i32 {
(self.vtable.fn_add)(self.base_type, s)
}
}
总结一下,我们将指向 base type 的引用替换为了指向同一块数据的合适类型的指针,从而擦除了原始的类型。无论是在宽引用(&dyn Trait
、&mut dyn Trait
)中,还是在虚表的函数指针中,都是采用这种方法。在这过程中编译器保证不出现 ABI 不匹配(ABI mismatch)。
译注:这里“合适类型的指针”的意思是,无论是指向代码段里的函数,还是指向堆上的数据,需要根据情况为指针选择正确的类型。例如虚表中指向
look()
方法的函数指针,就需要符合look()
本身的函数签名;而在DynTraitRef
里指向变量本身的base_type
指针,就需要是*const ()
类型。
这里再次强调下:上述代码只是一种可能的实现方式,目的在于帮助理解和进行探讨,而非编译器中的确切实现。
关于这个话题还有另一篇博客文章可以一读。不过请注意,它写于 2015 年,自那时以来 Rust 在许多方面都发生了变化。例如,特征对象过去的语法仅仅是 Trait
而不是 dyn Trait
。因此,你必须根据上下文判断作者是在讨论特征还是特征对象类型。
3. 实现其他 receiver 类型的方法
让我们看一下另一个方法,其函数签名的 receiver 的类型不是对自身的引用,而是 Box
:
trait Trait {
fn eat_box(self: Box<Self>);
}
如果我们要为特征对象实现 Trait
,那么对于方法 eat_box
,其 self
的类型就应当是 Box<dyn Trait>
了,这时我们应该怎么做呢?从内存结构来看,Box<BaseType /* : Sized */>
是一个窄指针,而 Box<dyn Trait>
是一个宽指针,非常类似于 &mut dyn Trait
(尽管也有一些不同: Box
拥有其指向对象的所有权,而可变引用 &mut
并非如此)。因此这里整体的思路也是类似于 &mut dyn Trait
的:
// 下面的代码仍然只是为了演示目的
impl Trait for dyn Trait + '_ {
fn eat_box(self: Box<Self>) {
let BoxRepresentation { base_type, vtable } = self;
let boxed_type = Box::from_raw(base_type);
(vtable.fn_eat_box)(boxed_type);
}
}
简而言之,对于一些受支持的 receiver 类型,例如 &self
、Box<Self>
等,编译器能够自动地将它们从类型擦除的形式(如 Box<Self>
,这里 Self
是特征对象)转换成对 base type(Box<BaseType>
)ABI 兼容的形式。现阶段,这个转换操作与特征 DispatchFromDyn
有关。目前所有受支持的类型都在这个特征的文档中列出了,注意其中有些类型是只在 unstable 的 arbitrary_self_types
feature 下受支持的。
4. Supertrait 的实现
我们稍后会更详细地看一下 supertraits(TODO:),但在这里我们先给出一个简单的结论:当你有一个 supertrait 时:
trait SuperTrait { /* ... */ }
trait Trait: SuperTrait { /* ... */ }
那么编译器也会自动为 dyn Trait
实现 SuperTrait
,正如他会自动实现 Trait
一样。此时的虚表中会包含 SuperTrait
的方法。
5. Box<dyn Trait>
和 &dyn Trait
并不自动实现 Trait
事实上,Box<dyn Trait>
和 &dyn Trait
并不会自动实现 Trait
,这可能会让人很惊讶。为什么呢?原因很简单:这并不总是可能的。
正如我们稍后会讲到的(TODO:),一个特征可能含有无法通过 dyn Trait
分发的方法,但这些方法又必须为所有的定长类型(Sized
)实现。一个典型的例子是没有 receiver 的关联函数:
trait Trait {
fn no_receiver() -> String where Self: Sized;
}
对于这样的关联函数,编译器没有办法生成函数体,无法实现这个这个方法,进而导致不能实现 Trait
。
另外,分发方法的 receiver 并不总是有意义的:
trait Trait {
fn takes_mut(&mut self);
}
对于 &dyn Trait
,编译器可以从宽指针中获取 &BaseType
,但无法获取到 &mut BaseType
,所以编译器无法为 &dyn Trait
实现 Trait::takes_mut()
。
同样地,一个 Arc<dyn Trait>
没有办法调用 Box<dyn Trait>
(译注:Arc<dyn Triat>
并不拥有对象的所有权),反之亦然,等等。‘
5.1. 可以手动实现
如果 Trait
是一个本地特征(local trait),那么我们可以为 Box<dyn Trait + '_>
等实现它,就和其他类型一样。不过要小心,很容易误写成递归定义!我们稍后(TODO:)会通过一个例子来讲解这一点。
此外,&T
、&mut T
和 Box<T>
是基础类型(fundamental type),这使得在孤儿规则(决定你可以手动实现哪些特征)方面,它们的行为与 T
相同。另外,如果 Trait
是一个本地特征(local trait),则 dyn Trait + '_
是一个本地类型(local type)。
上述性质加在一起,意味着我们可以为 Box<dyn Trait + '_>
(和其他含有基础类型的 wrapper)手动实现其他特征,包括但不限于 Trait
。
我们稍后也会有这方面的例子(TODO:)。
不幸的是,Rc
、Arc
等并不是基础类型,因此并非所有场景都能这么干。
译注:这里额外解释一下几个名词:
本地特征(local trait):在当前 crate 中定义的特征。一个特征是否是本地的与类型参数无关。例如,当前 crate 中定义了特征
Foo<T, U>
,那么Foo
总是 local 的,无论T
或U
是什么类型。本地类型(local Type):在当前 crate 中定义的结构体、枚举或联合。同样不受类型参数的影响。
struct Foo
被视为 local,但Vec<Foo>
不是。LocalType<ForeignType>
是 local。为类型或特征取别名(alias)不影响其本地性。被覆盖类型(Covered Type):作为另一种类型的参数的类型。例如,
T
是没有被覆盖的,但Vec<T>
中的T
是被覆盖的。本名词只与类型参数有关。覆盖实现(Blanket impl):如果一个对类型
T
的 impl,T
在其中未被覆盖,那么这个实现就被称为 blanket impl。impl<T> Foo for T
,impl<T> Bar<T> for T
,impl<T> Bar<Vec<T>> for T
, 和impl<T> Bar<T> for Vec<T>
(TODO: why?)都被认为是 blanket impl。然而,impl<T> Bar<Vec<T>> for Vec<T>
不是一个全覆盖实现,因为在这个实现中出现的所有T
实例都被Vec
覆盖了。基础类型(Fundamental Type):你不能向后兼容地为这些类型增加覆盖实现。这包括
&
、&mut
和Box
。任何时候,若类型T
是 local 的,那么&T
、&mut T
和Box<T>
也被认为是 local 的。基础类型不能覆盖其他类型。在任何语境下,当我们使用“被覆盖类型 covered type”一词时,&T
、&mut T
和Box<T>
均不被考虑为被覆盖。
6. 编译器提供的实现不能被直接覆写(override)
编译器为 dyn Trait
提供的 Trait
实现是不能被用户代码中的实现覆盖的。如果我们尝试这么做,会导致编译报错:
trait Trait {}
impl Trait for dyn Trait + '_ {}
如果你为 Trait
定义了一个覆盖实现(前文提到的 blanket impl),即使 dyn Trait
也属于这个覆盖实现的范围,它也还是会被忽略,实际使用的仍然是编译器自动生成的实现。例如:
trait Trait {
fn hi(&self) {
println!("Hi from {}!", type_name::<Self>());
}
}
// 最简单的例子:为所有类型都实现Trait
impl<T: ?Sized> Trait for T {}
let dt: &dyn Trait = &();
// 打印 "Hi from ()!" 而不是 "Hi from dyn Trait!"
dt.hi();
// 这种写法的结果也一样
<dyn Trait as Trait>::hi(dt);
译注:在上面这个例子里,如果是使用编译器提供的实现,那么对于
dt
的内存结构,其宽指针的数据指针指向了()
,因此会是通过虚表对()
调用hi()
,从而输出"Hi from ()!"。而如果是使用用户代码中的覆盖实现,即为所有T
都实现的Trait
,那么根据hi()
函数体的写法,应该是输出Self
的类型名,即dyn Trait
这条规则甚至适用于更复杂的实现,也适用于 dyn Trait
的 supertrait 实现。
我们后面将会看到,这个性质是有用的(TODO:)。但不幸的是,目前存在一些 bug,导致在某些场景下,编译器生成的实现并不能比用户手写的覆盖实现拥有更高的优先级。这些 bug 的最终解决方式尚未确定,有可能未来 rust 将不会允许某些覆盖实现,或者某些特征将不再是 dyn-safe。(而普通的写法,比如上面的简单例子,由于实在太泛用了,很大概率不会被废弃。)
7. 编译器提供的实现不能被绕过
你可能知道,如果存在多个同名且同 receiver 的方法,在进行方法查找时,类型固有的方法(inherent method)比在特征中定义的方法拥有更高的优先级:
trait Trait { fn method(&self) { println!("In trait Trait"); } }
struct S;
impl Trait for S {}
impl S { fn method(&self) { println!("In impl S"); } }
fn main() {
let s = S;
s.method(); // 输出 In impl S
// 如果你想使用特征 Trait 中的方法,你可以这样做
<S as Trait>::method(&s); // 输出 In trait Trait
}
不幸的是,这个性质不适用于 dyn Trait
,如果这种情况出现在特征对象上,编译器会认为这个方法调用是模糊不清的:
trait Trait {
fn method(&self) {}
fn non_dyn_dispatchable(&self) where Self: Sized {}
}
impl dyn Trait + '_ {
fn method(&self) {}
fn non_dyn_dispatchable(&self) {}
}
fn foo(d: &dyn Trait) {
d.method();
d.non_dyn_dispatchable();
}
编译报错:
error[E0034]: multiple applicable items in scope
--> src/main.rs:18:7
|
18 | d.method();
| ^^^^^^ multiple `method` found
|
note: candidate #1 is defined in an impl for the type `dyn Trait`
--> src/main.rs:13:5
|
13 | fn method(&self) {}
| ^^^^^^^^^^^^^^^^
note: candidate #2 is defined in the trait `Trait`
--> src/main.rs:8:5
|
8 | fn method(&self) {}
| ^^^^^^^^^^^^^^^^
help: disambiguate the method for candidate #2
|
18 | Trait::method(&d);
| ~~~~~~~~~~~~~~~~~
正如报错信息里提到的,我们可以用 Trait::method
这个语法来指定使用特征包含的方法;但是,对于特征对象的固有方法(即 impl dyn Trait + '_
中的部分),rust 并没有提供专门的语法来让用户调用。即使你试图隐藏该特征(TODO:),这些方法也是无法到达的 dead code。
显然,制定 rust 和编译器规范的大佬们在设计上述规则时,他们的想法是“特征中定义的方法就应该是 dyn Trait
的固有方法”。但这相当不幸,因为它排除了一些覆盖操作的可能,正如上面 non_dyn_dispatchable
尝试去做的那样。更多信息见 issue 51402。
不过,为 dyn Trait
实现一些不会覆盖 Trait
的方法是可行的。例如:
impl dyn Trait + '_ {
fn some_other_method(&self) {}
}
fn bar(d: &dyn Trait) {
d.some_other_method();
}
8. 一个少见的例外:dyn Trait
可以不实现 Trait
某些特征约束只有在你尝试使用该特征时才会检查,即使当该特征被认为是对象安全的。结果就是,你甚至可以创建一个没有实现 Trait
的 dyn Trait
!参考 issue 88904
trait Iterable
where
for<'a> &'a Self: IntoIterator<
Item = &'a <Self as Iterable>::Borrow,
>,
{
type Borrow;
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Borrow> + '_> {
Box::new(self.into_iter())
}
}
impl<I: ?Sized, Borrow> Iterable for I
where
for<'a> &'a Self: IntoIterator<Item = &'a Borrow>,
{
type Borrow = Borrow;
}
fn example(v: Vec<String>) {
// 这段代码可以编译,证明我们可以创建 `dyn Iterable`
// (即该特征是对象安全的并且 `v` 可以被强制转换)
let dt: &dyn Iterable<Borrow = String> = &v;
// 但这会导致编译错误,因为 `&dyn Iterable` 不满足特征
// 约束,因此 `dyn Iterable` 实际上不实现 `Iterable`!
for item in dt.iter() {
println!("{item}");
}
}
对于上面的例子,其实也有解决方案,补充一个实现,让 dyn Iterable
满足应有的约束,就能解决这个诡异的问题。如果这种操作不可行的话,我们就只能放弃该特征约束,或者放弃该特征的 dyn-safe 了。