4.1 dyn Trait 概述
6 分钟阅读
译文 · 基于 Learning Rust
dyn Trait 概述
原文链接: https://quinedot.github.io/rust-learning/dyn-trait-overview.html
什么是 dyn Trait?
dyn Trait 是编译器提供的、实现了 Trait 的类型。Trait 的 Sized 实现类型可以被强制转换为 dyn Trait,在此过程中擦除原始基础类型。Trait 的不同实现可能有不同大小,因此 dyn Trait 没有静态已知大小。这意味着它不实现 Sized,我们称此类类型为「非大小」或「动态大小类型(DST)」。
每个 dyn Trait 值都是类型擦除某个已有值的结果。不能仅从 trait 定义创建 dyn Trait;必须有可强制转换的实现基础类型。
Rust 目前不支持传递非大小参数、返回非大小值或拥有非大小局部变量。因此与 dyn Trait 交互时,通常需要某种间接:Box<dyn Trait>、&dyn Trait、Arc<dyn Trait> 等。
实际上,间接还有另一个原因。这些间接类型是或包含指向擦除类型的宽指针,由指向值的指针和指向静态虚表(vtable) 的第二个指针组成。虚表包含值的大小、析构函数指针、Trait 方法的指针等。虚表实现动态分发,使不同的 dyn Trait 值能将方法调用分派到不同的擦除基础类型对 Trait 的实现。
dyn Trait 也称为「trait 对象(trait object)」。
也可以有 dyn Trait + Send + Sync 等对象。Send 和 Sync 是自动 trait(auto-trait),trait 对象可包含任意数量的此类自动 trait 作为额外约束。每个不同的 Trait + AutoTraits 集合都是不同类型(但命名顺序无关)。
然而,trait 对象中只能有一个非自动 trait,也称为主 trait(principal trait)。因此以下不行:
| |
话虽如此,通常可用子 trait/超 trait 模式绕过此限制。
Trait 对象生命周期
坦白说:我们说 dyn Trait 是一种类型时不够精确。dyn Trait 是类型构造器:它由生命周期参数化,类似引用。因此单独的 dyn Trait 不是类型,dyn Trait + 'a(某个具体生命周期 'a)才是类型。
生命周期通常可以省略,稍后探讨。但它始终是类型的一部分,就像每个引用类型的生命周期一样,即使被省略。
关联类型
若 trait 有非泛型关联类型,这些关联类型通常成为 dyn Trait 的命名参数:
| |
我们在后续章节更详细探讨 dyn Trait 中的关联类型。
dyn Trait 不是什么
dyn Trait 不是 Sized
我们已提到 dyn Trait 不是 Sized。
再强调:泛型参数有隐式 Sized 约束。因此在泛型上下文中使用 dyn Trait 时,可能需要用 : ?Sized 移除隐式约束。
| |
若泛型仅用于 &T 或 Box<T> 等间接之后,可考虑用 + ?Sized 放宽隐式约束。另一方面,可能需要自己将 T 强制转换为 dyn Trait,而 T 不是 Sized 时无法做到。因此并非总是正确选择!
dyn Trait 既不是泛型也不是动态类型
给定具体生命周期 'a,dyn Trait + 'a 是静态已知类型。擦除的基础类型不是静态已知的,但别因此混淆:dyn Trait 本身是独立类型,该类型在编译时已知。
例如这两个函数签名:
| |
泛型情况下,对传入的每个类型 T 都会生成函数的一个版本。这种为每种类型在编译时生成新函数称为单态化(monomorphization)。(旁注:生命周期在编译时被擦除,不会单态化。)
你甚至可以创建指向不同版本的函数指针:
| |
即函数项类型由某个 T: Trait 参数化。
相比之下,结果库中永远只有一个 not_generic 函数。Trait 的基础实现类型必须在传入函数前类型擦除为 dyn Trait + '_。函数类型不由泛型类型参数化。
类似地:
| |
bx: Box<T> 不是 Box<dyn Trait>。它是专门指向堆上分配的 T 的瘦拥有指针。因为此处 T 有隐式 Sized 约束,我们可以将 bx 强制转换为 Box<dyn Trait + '_>。但那是转换为不同类型的 Box:宽拥有指针,擦除了 T 并包含对应 vtable 指针。
我们将在后续章节探讨泛型与 dyn Trait 交互的更多细节。
你可能疑惑为何能在 &dyn Trait 或 Box<dyn Trait> 等上使用 Trait 的方法,尽管未声明该约束。原因类似于为何能在 String 上使用 Display 方法而无需声明该约束:类型静态已知,编译器识别 dyn Trait 实现 Trait,就像识别 String 实现 Display。trait 约束用于泛型,而非具体类型。
(事实上,Box<dyn Trait> 不会自动实现 Trait,但 deref 强制转换通常能处理该情况。对许多 std trait,Box<dyn Trait> 也显式实现了该 trait;我们稍后也会探讨其形式。)
此外,dyn Trait 的方法 trait 像 dyn Trait 类型的固有方法,因此即使未导入 Trait 也可使用。
作为具体类型,你也可以为 dyn Trait 实现方法(前提是 Trait 在本地 crate),甚至为 dyn Trait 实现其他 trait(见部分示例)。
dyn Trait 不是超类型
因为可将基础类型强制转换为 dyn Trait,人们常以为 dyn Trait 是所有可强制转换实现类型的某种超类型。trait 约束与生命周期约束语法相同,以及「向上转型」「向下转型」术语的混用,可能加剧混淆。
但从基础类型到 dyn Trait 的强制转换是 unsizing 强制转换,而非子到超类型转换;强制发生在代码中静态已知的位置,也可能改变涉及类型的布局。例如可能将瘦指针变为宽指针。
相关地,trait Trait 不是类。没有实现类型就无法创建 dyn Trait(没有内置构造函数),给定类型可实现许多 trait。为避免混淆,我建议不要将基础类型称为 trait 的「实例」。它只是实现 Trait 的类型,独立于 trait 存在。创建 String 时,我创建的是 String,不是「Display(以及 Debug、Write、ToString 等)的实例」。
读到「Trait 的实例」时,我假定变量是某种 dyn Trait,而非实现 Trait 的未擦除基础类型。
为 dyn Trait 实现某物,不会为所有其他 T: Trait 实现。实际上只为 dyn Trait 本身实现。为 dyn Trait + Send 实现某物,也不会为 dyn Trait 实现,反之亦然;它们也是独立、不同的类型。
Rust 中有办法模拟动态类型,稍后探讨。我们还会探讨超 trait 的作用(尽管名称如此,仍不定义子/超类型关系)。
Rust 中唯一的子类型涉及生命周期和对生命周期高阶的类型。
(吹毛求疵的自我纠正:trait 对象有生命周期,因此在该意义上是超类型。但这与大多数 Rust 学习者混淆的概念不同;与实现类型之间没有超类型关系。)
dyn Trait 并非普遍适用
细节在各自章节,简而言之,不能总是将 Trait 的实现类型强制转换为 dyn Trait。trait 和实现类型都必须满足特定条件。
总结
dyn Trait + 'a 是
- 由擦除
Trait实现类型创建的具体、静态已知类型 - 通过指向类型擦除值和静态 vtable 的宽指针使用
- 动态大小(非大小,不实现
Sized) - 通过动态分发实现
Trait - 不是所有实现类型的超类型
- 不是动态类型
- 不是泛型
- 不能从所有值创建
- 并非所有 trait 都可用