Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trait:定义共享行为

Trait 类似于其他语言中通常称为“接口“的特性,尽管有一些差异。Trait 允许我们以一种抽象的方式定义共享行为。我们可以使用 trait 约束来指定泛型类型是具有特定行为的任何类型。

注意:Trait 类似于其他语言中的接口,但有一些差异。

定义 Trait

类型的行为由我们可以对该类型调用的方法组成。不同类型共享相同的行为,如果我们可以对所有这些类型调用相同的方法。Trait 定义是一种将方法签名分组在一起的方法,用于定义实现某些目的所需的一组行为。

例如,假设我们有多个类型,它们都持有某种文本:NewsArticle 类型持有新闻故事,Tweet 类型持有最多 280 个字符的推文,以及可能有一些元数据。我们希望对这些类型中的每一个都能够显示摘要。因此,我们希望能够对每个类型调用 summarize 方法来获取该摘要。让我们看看如何在 trait 中表达这一点。

trait Summary {
  function summarize(self: &Self) -> String
}

在这里,我们使用 trait 关键字声明一个 trait,后面跟着 trait 的名称,在这种情况下是 Summary。在大括号内部,我们声明描述实现这个 trait 的类型的行为的方法签名,在这种情况下是 function summarize(self: &Self) -> String

在方法签名之后,而不是在大括号内提供一个实现,我们使用分号。每个实现这个 trait 的类型都必须提供自己的 summarize 方法的自定义行为。编译器将强制任何具有 Summary trait 的类型都具有完全此签名的 summarize 方法。

实现 Trait 在类型上

现在我们已经定义了 Summary trait,我们可以在我们的媒体聚合器类型上实现它。

type NewsArticle = {
  headline: String,
  location: String,
  author: String,
  content: String
}

impl Summary for NewsArticle {
  function summarize(self: &NewsArticle) -> String {
    String::format("{}-{}, by {} ({})", self.headline, self.location, self.author, self.content)
  }
}

type Tweet = {
  username: String,
  content: String,
  reply: boolean,
  retweet: boolean
}

impl Summary for Tweet {
  function summarize(self: &Tweet) -> String {
    String::format("{}: {}", self.username, self.content)
  }
}

在类型上实现 trait 类似于实现常规方法,只是我们在 impl 之后添加 trait 名称,然后使用 for 关键字,后面跟着我们正在为其实现 trait 的类型的名称。在 impl 块中,我们放置 trait 定义中定义的方法签名。我们不是在每个签名后添加分号,而是在大括号中填写方法体,以指定我们希望 trait 的方法对特定类型具有的行为。

一旦我们实现了 trait,我们就可以像调用非 trait 方法一样在 NewsArticleTweet 的实例上调用 trait 方法:

let tweet = {
  username: String::from("horse_ebooks"),
  content: String::from("当然,伙计们,你可能已经知道了"),
  reply: false,
  retweet: false
}
println("1 条新推文: {}", tweet.summarize())

这个代码打印 1 条新推文: horse_ebooks: 当然,伙计们,你可能已经知道了

Trait 作为参数

现在我们知道如何定义和实现 trait 了,让我们看看如何使用 trait 来定义接受许多不同类型的函数。例如,我们可以定义一个 notify 函数,该函数在其 item 参数上调用 summarize 方法,该参数是实现 Summary trait 的某种类型。为此,我们使用 impl Trait 语法:

function notify(item: impl Summary) {
  println("突发新闻!{}", item.summarize())
}

我们可以使用 impl 语法,而不是具体类型作为参数的类型,而是使用 trait 名称。这个参数接受实现了我们指定的 trait 的任何类型。在 notify 函数体中,我们可以调用来自 Summary trait 的任何方法,包括 summarize。我们可以调用 notify 并传入 NewsArticleTweet 的实例。使用具体类型(如 Stringinteger)调用此函数的代码将无法编译,因为这些类型不实现 Summary

Trait Bound 语法

impl Trait 语法是 trait bound 的语法糖,看起来像这样:

function notify<T: Summary>(item: T) {
  println("突发新闻!{}", item.summarize())
}

这与上一个示例等效,但稍微冗长一些。我们将 trait bound 与泛型类型参数的声明放在一起,放在尖括号内,在冒号之后。

使用 trait bound 语法的 impl Trait 语法很方便,在简单的情况下使代码更简洁。trait bound 语法可以在更复杂的情况下表达更多内容,例如,我们可以强制两个参数具有相同的类型。这只有在使用 trait bound 时才有可能:

function notify<T: Summary>(item1: T, item2: T) {
  // ...
}

我们指定的泛型类型 T 同时指定了 item1item2 的类型,它们必须是实现 Summary trait 的相同具体类型。如果我们使用 impl Trait 语法,我们不能这样做。

通过 + 指定多个 Trait Bound

我们也可以指定多个 trait bound。例如,如果我们想要求 notify 中的 item 既具有 summarize 方法又具有 Display trait,我们可以使用 + 语法:

function notify(item: impl Summary + Display) {
  // ...
}

+ 语法也与 trait bound 上的泛型类型参数一起使用:

function notify<T: Summary + Display>(item: T) {
  // ...
}

通过这两个 trait bound,notify 的主体可以调用 summarize 并使用 {} 格式化 item

通过 where 子句简化 Trait Bound

使用多个 trait bound 可能会有很多括号,每个泛型类型的 trait bound 列表可能会变得很长且难以阅读。出于这个原因,X 语言在函数签名之后有一个可选的 where 子句用于 trait bound。所以与其这样写:

function some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> integer {
  // ...
}

我们可以使用 where 子句:

function some_function<T, U>(t: T, u: U) -> integer
  where T: Display + Clone,
        U: Clone + Debug
{
  // ...
}

这个函数签名不那么杂乱:函数名、参数列表和返回类型彼此靠近,类似于没有许多 trait bound 的函数。

返回实现 Trait 的类型

我们还可以在返回位置使用 impl Trait 语法,以返回实现 trait 的某种类型的值:

function returns_summarizable() -> impl Summary {
  {
    username: String::from("horse_ebooks"),
    content: String::from("当然,伙计们,你可能已经知道了"),
    reply: false,
    retweet: false
  }
}

通过使用 impl Summary 作为返回类型,我们指定 returns_summarizable 函数返回实现 Summary trait 的某种类型,但没有指定具体类型。在这个例子中,returns_summarizable 返回一个 Tweet,但调用该函数的代码不需要知道这一点。

能够在闭包和迭代器的上下文中指定仅通过 impl Trait 语法知道实现某个 trait 的返回类型特别有用,我们将在第 10 章中介绍。闭包和迭代器创建只有编译器知道或指定起来非常冗长的类型。impl Trait 语法允许你简洁地指定一个函数返回实现 Iterator trait 的某种类型。

但是,你只能在返回单个类型时使用 impl Trait。例如,如果你有返回 NewsArticleTweet 的代码,两者都实现 Summary,那么你不能使用 impl Summary 作为返回类型。

使用 Trait Bound 有条件地实现方法

通过在 impl 块上使用带有泛型类型参数的 trait bound,我们可以有条件地仅针对实现指定 trait 的类型实现方法。

type Pair<T> = {
  x: T,
  y: T
}

function Pair::new<T>(x: T, y: T) -> Pair<T> {
  { x: x, y: y }
}

impl<T: Display + PartialOrd> Pair<T> {
  function cmp_display(self: &Pair<T>) {
    if self.x >= self.y {
      println("最大的成员是 x = {}", self.x)
    } else {
      println("最大的成员是 y = {}", self.y)
    }
  }
}

Pair<T> 类型总是实现 new 函数。但是,只有当 T 实现了允许比较的 PartialOrd trait 和允许打印的 Display trait 时,它才会实现 cmp_display 方法。

我们也可以有条件地为实现另一个 trait 的任何类型实现 trait。在实现 trait 时,在满足 trait bound 的任何类型上实现 trait 称为全覆盖实现,它们在 X 语言标准库中被广泛使用。例如,标准库为实现 Display trait 的任何类型实现 ToString trait。标准库中的这个 impl 块看起来类似于以下代码:

impl<T: Display> ToString for T {
  // --snip--
}

因为标准库有这个全覆盖实现,我们可以在实现 Display trait 的任何类型上调用 ToString trait 中定义的 to_string 方法。例如,我们可以将整数变成字符串,因为整数实现了 Display

let s = 3.to_string()

总结

Trait 和 trait bound 允许我们以抽象的方式定义共享行为,并让我们在不导致代码重复的情况下利用这种共享行为。它们还允许我们指定泛型类型将具有特定行为,而不仅仅是任何类型。

现在我们已经讨论了 X 语言中泛型和 trait 的一些主要特性,让我们继续讨论类和面向对象编程!