特征(Traits):定义共享行为

特征(trait)定义了特定类型所具有的功能,并且可以与其他类型共享。我们可以使用特征以抽象的方式定义共享行为。我们可以使用特征约束(trait bounds)来指定泛型类型可以是任何具有特定行为的类型。

注意:特征(Traits)类似于其他语言中通常称为接口(interfaces)的功能,尽管存在一些差异。

定义特征(Trait)

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

例如,假设我们有多个结构体(structs),用于保存各种类型和数量的文本:`NewsArticle` 结构体用于保存特定地点存档的新闻报道,而 `Tweet` 最多可以包含 280 个字符以及指示它是新推文、转推还是回复另一条推文的元数据。

我们想要创建一个名为 `aggregator` 的媒体聚合库 crate,它可以显示可能存储在 `NewsArticle` 或 `Tweet` 实例中的数据的摘要。为此,我们需要每种类型都提供摘要,并且我们将通过在实例上调用 `summarize` 方法来请求该摘要。列表 10-12 显示了表达此行为的公共 `Summary` 特征(trait)的定义。

文件名:src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

列表 10-12:一个 `Summary` 特征(trait),由 `summarize` 方法提供的行为组成

在这里,我们使用 `trait` 关键字声明一个特征(trait),然后是特征的名称,在本例中为 `Summary`。我们还将该特征声明为 `pub`,以便依赖于此 crate 的其他 crate 也可以使用此特征,我们将在几个示例中看到这一点。在花括号内,我们声明了方法签名,这些签名描述了实现此特征的类型的行为,在本例中为 `fn summarize(&self) -> String`。

在方法签名之后,我们使用分号而不是在花括号内提供实现。每个实现此特征的类型都必须为该方法的函数体提供自己的自定义行为。编译器将强制执行任何具有 `Summary` 特征的类型都必须具有使用此签名精确定义的 `summarize` 方法。

一个特征(trait)在其函数体中可以有多个方法:方法签名每行列出一个,并且每行以分号结尾。

在类型上实现特征(Trait)

现在我们已经定义了 `Summary` 特征的方法的所需签名,我们可以在媒体聚合器中的类型上实现它。列表 10-13 显示了 `Summary` 特征在 `NewsArticle` 结构体上的实现,该实现使用标题、作者和位置来创建 `summarize` 的返回值。对于 `Tweet` 结构体,我们将 `summarize` 定义为用户名后跟推文的全部文本,假设推文内容已被限制为 280 个字符。

文件名:src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

列表 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summary` 特征(trait)

在类型上实现特征(trait)类似于实现常规方法。不同之处在于,在 `impl` 之后,我们放置要实现的特征名称,然后使用 `for` 关键字,然后指定要为其实现特征的类型的名称。在 `impl` 代码块中,我们放置特征定义已定义的方法签名。我们使用花括号并填充方法体,而不是在每个签名后添加分号,并在方法体中填充我们希望特征的方法对于特定类型具有的特定行为。

现在库已在 `NewsArticle` 和 `Tweet` 上实现了 `Summary` 特征,crate 的用户可以像调用常规方法一样在 `NewsArticle` 和 `Tweet` 的实例上调用特征方法。唯一的区别是用户必须将特征以及类型都引入作用域。以下是一个二进制 crate 如何使用我们的 `aggregator` 库 crate 的示例

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

此代码打印 `1 new tweet: horse_ebooks: of course, as you probably already know, people`。

依赖于 `aggregator` crate 的其他 crate 也可以将 `Summary` 特征引入作用域,以便在其自己的类型上实现 `Summary`。需要注意的一个限制是,我们只能在特征或类型(或两者)对于我们的 crate 是本地的情况下,才能在类型上实现特征。例如,我们可以将标准库特征(如 `Display`)在自定义类型(如 `Tweet`)上实现,作为我们的 `aggregator` crate 功能的一部分,因为类型 `Tweet` 对于我们的 `aggregator` crate 是本地的。我们也可以在我们的 `aggregator` crate 中在 `Vec` 上实现 `Summary`,因为特征 `Summary` 对于我们的 `aggregator` crate 是本地的。

但是我们不能在外部类型上实现外部特征。例如,我们不能在我们的 `aggregator` crate 中在 `Vec` 上实现 `Display` 特征,因为 `Display` 和 `Vec` 都在标准库中定义,并且对于我们的 `aggregator` crate 不是本地的。此限制是称为 *一致性*(coherence)的属性的一部分,更具体地说是 *孤儿规则*(orphan rule),之所以这样命名是因为父类型不存在。此规则确保其他人的代码不会破坏您的代码,反之亦然。如果没有该规则,则两个 crate 可以为同一类型实现相同的特征,而 Rust 将不知道要使用哪个实现。

默认实现

有时,对于特征(trait)中的某些或所有方法具有默认行为而不是要求在每种类型上实现所有方法很有用。然后,当我们在特定类型上实现特征时,我们可以保留或覆盖每个方法的默认行为。

在列表 10-14 中,我们为 `Summary` 特征的 `summarize` 方法指定了默认字符串,而不是像我们在列表 10-12 中那样仅定义方法签名。

文件名:src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

列表 10-14:定义具有 `summarize` 方法默认实现的 `Summary` 特征(trait)

要使用默认实现来摘要 `NewsArticle` 的实例,我们使用 `impl Summary for NewsArticle {}` 指定一个空的 `impl` 代码块。

即使我们不再直接在 `NewsArticle` 上定义 `summarize` 方法,我们也提供了默认实现并指定 `NewsArticle` 实现了 `Summary` 特征。因此,我们仍然可以在 `NewsArticle` 的实例上调用 `summarize` 方法,如下所示

use aggregator::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

此代码打印 `New article available! (Read more...)`。

创建默认实现不需要我们更改列表 10-13 中 `Tweet` 上 `Summary` 的实现中的任何内容。原因是覆盖默认实现的语法与实现没有默认实现的特征方法的语法相同。

默认实现可以调用同一特征(trait)中的其他方法,即使这些其他方法没有默认实现。这样,一个特征可以提供许多有用的功能,并且仅要求实现者指定其中的一小部分。例如,我们可以将 `Summary` 特征定义为具有一个需要实现的 `summarize_author` 方法,然后定义一个具有默认实现并调用 `summarize_author` 方法的 `summarize` 方法

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

要使用此版本的 `Summary`,我们只需要在类型上实现特征时定义 `summarize_author`

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

在我们定义 `summarize_author` 之后,我们可以在 `Tweet` 结构体的实例上调用 `summarize`,并且 `summarize` 的默认实现将调用我们提供的 `summarize_author` 的定义。因为我们已经实现了 `summarize_author`,所以 `Summary` 特征为我们提供了 `summarize` 方法的行为,而无需我们编写更多代码。以下是它的外观

use aggregator::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

此代码打印 `1 new tweet: (Read more from @horse_ebooks...)`。

请注意,无法从同一方法的覆盖实现中调用默认实现。

将特征(Traits)作为参数

现在您已经知道如何定义和实现特征,我们可以探索如何使用特征来定义接受多种不同类型的函数。我们将使用我们在列表 10-13 中在 `NewsArticle` 和 `Tweet` 类型上实现的 `Summary` 特征来定义一个 `notify` 函数,该函数在其 `item` 参数上调用 `summarize` 方法,该参数是实现 `Summary` 特征的某种类型。为此,我们使用 `impl Trait` 语法,如下所示

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

对于 `item` 参数,我们指定 `impl` 关键字和特征名称,而不是具体类型。此参数接受任何实现指定特征的类型。在 `notify` 的函数体中,我们可以调用 `item` 上来自 `Summary` 特征的任何方法,例如 `summarize`。我们可以调用 `notify` 并传入 `NewsArticle` 或 `Tweet` 的任何实例。使用任何其他类型(例如 `String` 或 `i32`)调用该函数的代码将无法编译,因为这些类型未实现 `Summary`。

特征约束语法

`impl Trait` 语法适用于简单的情况,但实际上是较长形式的语法糖,称为 *特征约束*(trait bound);它看起来像这样

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

这种较长的形式等效于上一节中的示例,但更冗长。我们将特征约束与泛型类型参数的声明放在冒号之后和尖括号内。

`impl Trait` 语法很方便,并且在简单情况下可以使代码更简洁,而更完整的特征约束语法可以在其他情况下表达更多的复杂性。例如,我们可以有两个实现 `Summary` 的参数。使用 `impl Trait` 语法这样做看起来像这样

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

如果我们希望此函数允许 `item1` 和 `item2` 具有不同的类型(只要这两种类型都实现 `Summary`),则使用 `impl Trait` 是合适的。但是,如果我们想强制两个参数都具有相同的类型,则必须使用特征约束,如下所示

pub fn notify<T: Summary>(item1: &T, item2: &T) {

指定为 `item1` 和 `item2` 参数类型的泛型类型 `T` 约束了该函数,使得作为 `item1` 和 `item2` 参数传递的值的具体类型必须相同。

使用 `+` 语法指定多个特征约束

我们还可以指定多个特征约束。假设我们希望 `notify` 在 `item` 上使用显示格式以及 `summarize`:我们在 `notify` 定义中指定 `item` 必须同时实现 `Display` 和 `Summary`。我们可以使用 `+` 语法来实现

pub fn notify(item: &(impl Summary + Display)) {

`+` 语法对于泛型类型的特征约束也有效

pub fn notify<T: Summary + Display>(item: &T) {

指定了两个特征约束后,`notify` 的函数体可以调用 `summarize` 并使用 `{}` 格式化 `item`。

使用 `where` 子句使特征约束更清晰

使用过多的特征约束有其缺点。每个泛型都有其自己的特征约束,因此具有多个泛型类型参数的函数可能在函数名称和参数列表之间包含大量特征约束信息,从而使函数签名难以阅读。因此,Rust 具有在函数签名之后的 `where` 子句内指定特征约束的替代语法。因此,我们可以使用 `where` 子句,而不是编写如下代码

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

我们可以使用 `where` 子句,如下所示

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

此函数的签名不太混乱:函数名称、参数列表和返回类型紧密地结合在一起,类似于没有大量特征约束的函数。

返回实现特征(Traits)的类型

我们还可以在返回位置使用 `impl Trait` 语法来返回实现特征的某种类型的值,如下所示

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

通过对返回类型使用 `impl Summary`,我们指定 `returns_summarizable` 函数返回某种实现 `Summary` 特征的类型,而无需命名具体类型。在这种情况下,`returns_summarizable` 返回一个 `Tweet`,但是调用此函数的代码不需要知道这一点。

仅通过它实现的特征来指定返回类型的功能在闭包和迭代器的上下文中特别有用,我们将在第 13 章中介绍它们。闭包和迭代器创建只有编译器知道的类型或非常难以指定的类型。`impl Trait` 语法使您可以简洁地指定函数返回实现 `Iterator` 特征的某种类型,而无需写出非常长的类型。

但是,仅当您返回单个类型时才能使用 `impl Trait`。例如,以下代码返回 `NewsArticle` 或 `Tweet`,并将返回类型指定为 `impl Summary`,但它不起作用

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

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

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

由于围绕编译器中 `impl Trait` 语法的实现方式的限制,不允许返回 `NewsArticle` 或 `Tweet` 中的任何一个。我们将在 “使用允许不同类型值的特征对象”第 18 章的章节中介绍如何编写具有此行为的函数。

使用特征约束有条件地实现方法

通过将特征约束与使用泛型类型参数的 `impl` 代码块一起使用,我们可以为实现指定特征的类型有条件地实现方法。例如,列表 10-15 中的类型 `Pair` 始终实现 `new` 函数以返回 `Pair` 的新实例(回想一下 “定义方法”第 5 章的章节,`Self` 是 `impl` 代码块类型的类型别名,在本例中为 `Pair`)。但是在下一个 `impl` 代码块中,`Pair` 仅当其内部类型 `T` 实现启用比较的 `PartialOrd` 特征 *和* 启用打印的 `Display` 特征时,才实现 `cmp_display` 方法。

文件名:src/lib.rs

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

列表 10-15:根据特征约束有条件地在泛型类型上实现方法

我们还可以为任何实现另一个特征的类型有条件地实现一个特征。在满足特征约束的任何类型上实现特征称为 *总括实现*(blanket implementations),并在 Rust 标准库中广泛使用。例如,标准库在任何实现 `Display` 特征的类型上实现 `ToString` 特征。标准库中的 `impl` 代码块类似于以下代码

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

由于标准库具有此总括实现,因此我们可以在任何实现 `Display` 特征的类型上调用 `ToString` 特征定义的 `to_string` 方法。例如,我们可以像这样将整数转换为其对应的 `String` 值,因为整数实现了 `Display`

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

总括实现出现在特征的文档的 “Implementors” 部分中。

特征(Traits)和特征约束(trait bounds)使我们能够编写使用泛型类型参数来减少重复的代码,但也向编译器指定我们希望泛型类型具有特定行为。然后,编译器可以使用特征约束信息来检查与我们的代码一起使用的所有具体类型是否都提供了正确的行为。在动态类型语言中,如果我们在未定义该方法的类型上调用方法,则会在运行时收到错误。但是 Rust 将这些错误移至编译时,因此我们被迫在代码甚至能够运行之前修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做可以提高性能,而无需放弃泛型的灵活性。