从多态和代码复用的角度来看,在代码中将类型的共享行为和公共属性与其自身隔离 通常是一个好主意,并且能拥有专属于自己的方法。在这样做时,我们允许不同类型通过 通用属性互相关联,使我们能够为 API 编程,使其参数更通用或更具包容性。这意味着我 们可以接收具有这些通用属性的类型,而不仅限于某种特定类型。

类似 JavaC#的面向对象编程语言中,接口表达了相同的理念,我们可以在其中定 义多种类型能够实现的共享行为。例如,我们可以使用单个 sort 函数接收实现 Comparable 或者 Comparator 接口的元素列表,而不是使用多个 sort 函数接收整数值列表,以及用其他 函数接收字符串值列表。这使得我们可以将任何可比较(Comparable)的内容传递给 sort 函数。

Rust 也有一个类似且功能强大的结构,被称为特征。Rust 中的特征以多种形式存在, 我们将介绍一些最常见的形式并了解一些与它们简单交互的方式。此外,当特征与泛型搭 配使用时,可以限制传递到 API 的参数范围。我们将会对特征进行比较深入的了解。

特征

特征是一个元素,它定义了一组类型可以选择性实现的“契约”或共享行为。特征本身没有什么用,并且需要根据类型予以实现。特征有能力在不同类型之间建立关联,它们是许多语言特性的基础。例如闭包、运算符、智能指针、循环及编译期数据竞争效验等。Rust中相当多的高级语言特性要归功于调用某些类型实现的特征方法。为此,让我们看看如何在Rust中定义和使用特征。

假定我们正在构建一个可以播放音频和视频的简单多媒体播放器应用程序。要实现这 个应用程序,我们将通过运行 cargo new super_player 命令创建一个新的项目。为了表达特 征的理念,并简化它,在我们的 main.rs 文件中,会将音频和视频媒体表示为元组结构体,并将媒体名称作为字符串,如下所示:

  1. struct Audio(String);
  2. struct Video(String);
  3. fn main() {
  4. //stuff
  5. }

结构体 AudioVideo 至少需要有播放(play)和暂停(pause)功能,这是两者共享的功能,是我们使用特征的的好机会。在这里,将在单独模块 media.rs 中定义一个名为 Playable 的特征,其中包含两个方法,如下所示:

  1. trait Platable {
  2. fn play(&self);
  3. fn pause() {
  4. println!("Paused");
  5. }
  6. }
  1. 我们使用关键字 `trait` 创建一个特征,之后是其名称和一对花括号。在花括号内,我们 可以提供零个或多个方法,任何可实现特征的类型都应该对其提供具体实现。我们还可以 在特征中定义常量,所有实现者都可以共享它。实现者可以是任何结构体、枚举、基元类 型、函数及闭包,甚至特征。

你应该已经注意到 play 的特点,它会接收一个引用符号(&)和 self,但是没有函数体,并以分号作为结尾。self 只是 Self 的类型别名,它指的是实现特征的类型。由类型实现 此特征,并根据其用例定义函数。不过在特征中声明的方法也可以具有默认实现,就像上 述代码中的 pause 函数一样。pause 不会将 self 作为参数,因此它类似静态方法,不需要实现者的实例来调用它。

可以在特征中提供两种方法。

  • 关联方法:这些方法可以直接在实现特征的类型上使用,并不需要类型的实例来调 用。在主流的编程语言中,这也被称为静态方法,例如标准库的特征 FromStrfrom_str 方法。它是通过 String 实现的,因此允许你通过String::from_str("foo")&str 创建一个 String
  • 实例方法:这些方法需要将 self 作为其第一个参数。这仅适用于实现特征的类型实 例,self将指向实现特征的类型实例。它可以有 3种类型:self方法,被调用时会 用到实例;&self 方法,只对实例的成员(如果有的话)有读取权限;&mut self方 法,它具有对成员的可变访问权限。可以修改它们甚至用另一个实例替换它们。例 如标准库中的 AsRef 特征的 as_ref 方法是一个带有&self 的实例方法,并且旨在由 可以转换为引用或指针的类型实现。

现在,我们将在AudioVideo类型上实现前面的Playable特征,如下所示:

  1. struct Audio(String);
  2. struct Video(String);
  3. impl Playable for Audio {
  4. fn play(&self) {
  5. println!("Now playing: {}", self.0);
  6. }
  7. }
  8. impl Playable for Video {
  9. fn play(&self) {
  10. println!("Now playing: {}", self.0);
  11. }
  12. }
  13. fn main() {
  14. println!("Super player");
  15. }
  1. 我们使用关键字`impl`后跟特征名称来声明特征实现,随后是关键字`for`和希望实现的特征类型,其后的花括号用于编写特征实现。在花括号中,我们需要提供1方法的实现,并根据需要覆盖特征中存在的任何默认实现。对代码进行编译,得到以下错误提示信息:

用特征抽象行为 - 图1

上述错误提示信息突出了特征的一个重要特性:在默认情况下,特征是私有的。要能 够被其他模块或其他软件包调用,它们需要被声明为公有的。这需要两个步骤。首先,我 们需要将特征暴露给外部世界。为此,我们需要在 Playable 特征声明前面添加关键字 pub:

  1. pub trait Playable {
  2. fn play(&self);
  3. fn pause() {
  4. println!("Paused");
  5. }
  6. }
  1. 在公开了特征之后,我们需要使用关键字 `use` 将特征导入需要调用特征的模块的作用 域中。这将允许我们调用它的方法,如下所示:
  1. mod media;
  2. use crate::media::Playable;
  3. struct Audio(String);
  4. struct Video(String);
  5. impl Playable for Audio {
  6. fn play(&self) {
  7. println!("Now playing: {}", self.0);
  8. }
  9. }
  10. impl Playable for Video {
  11. fn play(&self) {
  12. println!("Now playing: {}", self.0);
  13. }
  14. }
  15. fn main() {
  16. println!("Super player");
  17. let audio = Audio("ambient_music.mp3".to_string());
  18. let video = Video("big_buck_bunny.mkv".to_string());
  19. audio.play();
  20. video.play();
  21. }

这样我们就可以播放媒体的音频和视频:

用特征抽象行为 - 图2

特征也可以在声明中表明它们依赖于其他特征——这是一种被称为特征继承的特性。 我们可以像下列代码那样声明继承性特征:

  1. trait Vehicle {
  2. fn get_price(&self) -> u64;
  3. }
  4. trait Car: Vehicle {
  5. fn model(&self) -> String;
  6. }
  7. struct TeslaRoadster {
  8. model: String,
  9. release_date: u16
  10. }
  11. impl TeslaRoadster {
  12. fn new(model: &str, release_date: u16) -> Self {
  13. Self { model: model.to_string(), release_date}
  14. }
  15. }
  16. impl Car for TeslaRoadster {
  17. fn model(&self) -> String {
  18. "Tesla Roadster I".to_string()
  19. }
  20. }
  21. fn main() {
  22. let my_roadster = TeslaRoadster::new("Tesla Roadster II", 2024);
  23. println!("{} is priced ad ${}", my_roadster.model, my_roadster.get_price());
  24. }
  1. 在上述代码中,我们声明了两个特征`:Vehicle`(更一般)和 `Car(更具体)``Car` 依赖于 `Vehicle`。因为 `TeslaRoadster` 是一辆车,我们为它实现了 `Car` 特征。另外,请注意 `TeslaRoadster` `new` 方法主体,它采用 `Self` 作为返回类型,取代了我们从 `new` 方法返回的 `TeslaRoadster` 实例。`Self` 只是特征的 `impl` 块中实现类型的简便类型别名,它还可以用于创建其他类型,例 如元组结构体和枚举,以及 `match` 表达式。让我们尝试对这段代码进行编译:

用特征抽象行为 - 图3

看到那个错误了吗?在其定义中,Car 特征指定了约束,任何实现特征的类型必须实 现 Vehicle 特征,即 Car: VehicleTeslaRoadster 未实现 Vehicle 特征,Rust 捕获了这个问题 并报告给我们。因此,我们必须实现 Vehicle,如下所示:

  1. impl Vehicle for TeslaRoadster {
  2. fn get_price(&self) -> u64 {
  3. 200_000
  4. }
  5. }

用特征抽象行为 - 图4

和面向对象语言相比,特征及其实现类似接口和实现这些接口的类。但是需要注意的是,特征与接口存在很大差异。

  • 尽管特征在 Rust 中具有一种继承形式,却没有具体实现。这意味着可以声明一个 名为 Panda 的特征,然后通过实现 Panda 的类型实现另一个名为 KungFu 的特征。 但是,类型本身并没有任何继承。因此采用的是类型组合而不是对象继承,它依赖 于特征继承来为代码中的任何实际的实体建模。
  • 你可以在任何地方编写特征实现的代码块,而且无须访问实际类型。
  • 你还可以基于内置的基元类型到泛型之间的任何类型实现自定义特征。
  • 在函数中不能隐式地将返回类型作为特征,就像在 Java 中可以将接口作为返回类 型,你必须返回一个被称为特征对象的东西,并且这种声明是显式的。当我们讨论 特征对象时,将会了解如何做到这一点。

impl Vehicle for TeslaRoadster 表示TeslaRoadster实现了Vehicle这个特征。

trait Car: Vehicle表示Car继承Vehicle实现 **<font style="color:rgb(13, 13, 13);">Car</font>** 的类型也必须实现 **<font style="color:rgb(13, 13, 13);">Vehicle</font>**


特征的多种形式

在前面的示例中,我们了解了最简单的特征形式,但这只是特征的冰山一角。当你开 始接触大型代码库中的特征时,将会遇到它的多种形式。因为程序的复杂度和要解决的问 题相比较,简单的特征形式可能并不适合我们使用。Rust 为我们提供了其他形式的特征, 可以很好地帮助我们为问题建模。我们将介绍一个标准库的特征,并尝试对它们进行分类, 以便了解何时需要使用它们。

标记特征

std::marker模块中定义的特征被称为标记特征(marker trait)。这种特征不包含任何方法,声明时只是提供特征名称和空的函数体。

标准库中的示例包括CopySendSync。它们被称为标记特征,因为它们用于简单地将类型标记为属于特定的组群,以获得一定程度的编译期保障。标准库中的两个这样的示例是SendSync特征,它们在适当的时候由语言为大多数类型自动实现,并确定哪些值可以安全地发送和跨线程共享。

简单特征

这可能是特征定义的最简单形式。我们已将它作为特征的简单定义进行了阐述:

  1. trait Foo {
  2. fn foo();
  3. }

标准库中的一个示例是 Default 特征,它主要是针对可以使用默认值初始化的类型实现的。

泛型特征

特征也可以是泛型。这在用户希望为多种类型实现特征的情况下非常有用:

  1. pub trait From<T> {
  2. fn from(T) -> Self;
  3. }

这样的两个例子是From<T>Into<T>特征,它们允许从某类型转换为类型T,反之亦然。当这些特征用作函数参数中的特征区间时,它们的作用尤为突出。当使用3个或者4个泛型声明它们时,通常特征会变得非常冗长。对于这种情况,我们可以使用关联类型特征。

关联类型特征

  1. trait Foo {
  2. type Out;
  3. fn get_value(self) -> Self:Out;
  4. }
  1. 这是泛型特征的更好选择,因为它们能够在特征中声明相关类型,例如前面代码中特 `Foo` 声明的 `Out` 类型。它们具有较少的类型签名,其优点在于,在实际的编程中,它们允许用户一次性声明关联类型,并在任何特征方法或函数中使用 `Self::Out` 作为返回类型或 参数类型。这消除了类型的冗余声明,与泛型特征的情况类似。关联类型特征的最佳用例 之一是 `Iterator` 特征。

**<font style="color:rgb(13, 13, 13);">type Out;</font>** 这样的语法通常出现在特征(<font style="color:rgb(13, 13, 13);">trait</font>)定义中,用来声明一个关联类型。关联类型是 <font style="color:rgb(13, 13, 13);">trait</font> 中定义的一种类型占位符,它允许在实现该 <font style="color:rgb(13, 13, 13);">trait</font> 的时候指定具体的类型。这提供了一种在 <font style="color:rgb(13, 13, 13);">trait</font> 本身不需要知道具体类型的情况下,使 <font style="color:rgb(13, 13, 13);">trait</font> 方法能够返回或使用这些类型的方法。

继承特征

我们已经在代码示例 trait_inheritance.rs 中看到了这些特征。与 Rust 中的类型不同,特征可以具有继承关系,例如:

  1. trait Bar {
  2. fn bar();
  3. }
  4. trait Foo: Bar {
  5. fn foo();
  6. }
  1. 在上述代码片段中,我们声明了一个特征 `Foo`,它依赖于父级特征 `Bar`。在 `Foo` 的定义中,要求用户在为类型实现 `Foo` 特征时必须为 `Bar` 特征提供实现。标准库中的这样一个示 例是 `Copy` 特征,它要求类型必须实现 `Clone` 特征。