深入理解特征对象之二:为 `dyn Trait` 实现 `Trait` 的原理

本文是 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 类型,例如 &selfBox<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 TBox<T> 是基础类型(fundamental type),这使得在孤儿规则(决定你可以手动实现哪些特征)方面,它们的行为与 T 相同。另外,如果 Trait 是一个本地特征(local trait),则 dyn Trait + '_ 是一个本地类型(local type)。

上述性质加在一起,意味着我们可以为 Box<dyn Trait + '_>(和其他含有基础类型的 wrapper)手动实现其他特征,包括但不限于 Trait

我们稍后也会有这方面的例子(TODO:)。

不幸的是,RcArc 等并不是基础类型,因此并非所有场景都能这么干。

译注:这里额外解释一下几个名词:

  • 本地特征(local trait):在当前 crate 中定义的特征。一个特征是否是本地的与类型参数无关。例如,当前 crate 中定义了特征 Foo<T, U>,那么 Foo 总是 local 的,无论 TU 是什么类型。

  • 本地类型(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):你不能向后兼容地为这些类型增加覆盖实现。这包括 &&mutBox。任何时候,若类型 T 是 local 的,那么 &T&mut TBox<T> 也被认为是 local 的。基础类型不能覆盖其他类型。在任何语境下,当我们使用“被覆盖类型 covered type”一词时,&T&mut TBox<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

某些特征约束只有在你尝试使用该特征时才会检查,即使当该特征被认为是对象安全的。结果就是,你甚至可以创建一个没有实现 Traitdyn 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 了。

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

发送评论 编辑评论


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