特质:定义共享行为

一个 trait 定义了特定类型具有的功能以及可以与其他类型共享的功能。我们可以使用 trait 以抽象的方式定义共享行为。我们可以使用 trait bounds 来指定泛型类型可以是具有某些行为的任何类型。

注意:Trait 与其它语言中经常称为 接口 的功能类似,尽管有一些不同。

定义一个特质

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

例如,假设我们有多个结构体,它们持有各种类型和数量的文本:NewsArticle 结构体保存在特定地点发布的一篇新闻故事,以及 Tweet 结构体,最多可以包含 280 个字符,以及指示它是新推文、转推还是回复其他推文的元数据。

我们想要制作一个名为 aggregator 的媒体聚合库 crate,可以显示可能存储在 NewsArticleTweet 实例中的数据摘要。为此,我们需要每种类型的摘要,并将通过在实例上调用 summarize 方法来请求该摘要。列表 10-12 显示了一个公共 Summary 特性的定义,该特性表达了这种行为。

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
Listing 10-12: A Summary trait that consists of the behavior provided by a summarize method

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

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

一个特质可以在其主体中拥有多个方法:方法签名按行列出,每行以分号结尾。

在类型上实现特征

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

Filename: 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)
    }
}
Listing 10-13: Implementing the Summary trait on the NewsArticle and Tweet types

在类型上实现特质类似于实现常规方法。不同之处在于在 impl 之后,我们放置我们想要实现的特质名称,然后使用 for 关键字,接着指定我们想要为其实现特质的类型的名称。在 impl 块内,我们放置特质定义已定义的方法签名。而不是在每个签名后添加分号,我们使用大括号并用我们希望特质的方法为特定类型具有的特定行为填充方法体。

现在库已经在 NewsArticleTweet 上实现了 Summary 特性,crate 的用户可以像调用普通方法一样在 NewsArticleTweet 的实例上调用这些特性方法。唯一的区别是用户必须将特性以及类型引入作用域。以下是一个二进制 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: 当然,正如你可能已经知道的,人们

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

但是我们不能为外部类型实现外部特征。例如,我们不能在我们的aggregator crate 中为 Vec<T> 实现 Display 特征,因为 DisplayVec<T> 都定义在标准库中,并不是我们 aggregator crate 的本地类型。这个限制是称为一致性的属性的一部分,更具体地说是孤儿规则,之所以这样命名是因为父类型不在场。这条规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有这条规则,两个 crate 可以为同一类型实现相同的特征,Rust 将不知道使用哪个实现。

默认实现

有时为特质中的某些或所有方法提供默认行为是有用的,而不是要求每个类型都实现所有方法。然后,当我们在这个特定类型上实现特质时,我们可以保留或覆盖每个方法的默认行为。

在清单 10-14 中,我们为 Summary 特性中的 summarize 方法指定了一个默认字符串,而不是像我们在清单 10-12 中那样仅定义方法签名。

Filename: 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)
    }
}
Listing 10-14: Defining a Summary trait with a default implementation of the summarize method

要使用默认实现来总结 NewsArticle 的实例,我们指定一个空的 impl 块,即 impl Summary for NewsArticle {}

即使我们不再直接在 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());
}

这段代码打印 新文章可用!(阅读更多...)

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

默认实现可以调用同一特质中的其他方法,即使这些其他方法没有默认实现。通过这种方式,一个特质可以提供大量有用的功能,而只需要实现者指定其中的一小部分。例如,我们可以定义 Summary 特质具有一个 summarize_author 方法,其实现是必需的,然后定义一个 summarize 方法,该方法具有默认实现并调用 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)
    }
}

要使用这个版本的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 条新推文: (阅读更多来自 @horse_ebooks...)

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

特质作为参数

现在你已经知道如何定义和实现特质,我们可以探讨如何使用特质来定义接受多种不同类型的函数。我们将使用在清单 10-13 中为 NewsArticleTweet 类型实现的 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 的函数体中,我们可以调用来自 Summary 特征的任何方法,例如 summarize。我们可以调用 notify 并传入任何 NewsArticleTweet 的实例。使用任何其他类型(如 Stringi32)调用该函数的代码将无法编译,因为这些类型没有实现 Summary

Trait Bound Syntax

impl Trait 语法适用于简单的情况,但实际上它是更长形式的语法糖,这种形式被称为 trait bound;它的样子是这样的:

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

这种较长的形式与上一节中的示例等效,但更为冗长。我们在泛型类型参数的声明后使用冒号和尖括号来放置特质边界。

impl Trait 语法在简单情况下非常方便,可以使代码更加简洁,而完整的 trait bound 语法在其他情况下可以表达更多的复杂性。例如,我们可以有两个实现 Summary 的参数。使用 impl Trait 语法如下所示:

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

使用 impl Trait 是合适的,如果我们希望这个函数允许 item1item2 具有不同的类型(只要这两种类型都实现了 Summary)。然而,如果我们希望强制两个参数具有相同的类型,我们必须使用一个特质边界,如下所示:

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

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

Specifying Multiple Trait Bounds with the + Syntax

我们也可以指定多个特征约束。假设我们希望 notify 使用显示格式化以及对 item 使用 summarize:我们在 notify 的定义中指定 item 必须同时实现 DisplaySummary。我们可以使用 + 语法来实现:

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

+ 语法在泛型类型的特征边界上也是有效的:

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

指定了两个 trait bound 后,notify 的主体可以调用 summarize 并使用 {} 来格式化 item

Clearer Trait Bounds with where Clauses

使用过多的 trait 约束有其缺点。每个泛型都有自己的 trait 约束,因此具有多个泛型类型参数的函数在函数名称和参数列表之间可能包含大量 trait 约束信息,使得函数签名难以阅读。因此,Rust 提供了在函数签名后的 where 子句中指定 trait 约束的替代语法。所以,与其写成这样:

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!()
}

这个函数的签名更加简洁:函数名、参数列表和返回类型紧密相邻,类似于没有大量特征约束的函数。

返回实现特质的类型

我们还可以在返回位置使用 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,
        }
    }
}

返回一个 NewsArticleTweet 是不允许的,因为有关 impl Trait 语法在编译器中的实现存在限制。我们将在第 18 章的 “使用允许不同类型的值的特质对象” 部分介绍如何编写具有此行为的函数。

使用特质边界有条件地实现方法

通过在使用泛型类型参数的 impl 块中使用特质约束,我们可以为实现指定特质的类型有条件地实现方法。例如,类型 Pair<T> 在清单 10-15 中始终实现 new 函数以返回 Pair<T> 的新实例(回想第 5 章的 “定义方法” 部分,Selfimpl 块类型的类型别名,在这种情况下是 Pair<T>)。但在下一个 impl 块中,Pair<T> 仅在其内部类型 T 实现了启用比较的 PartialOrd 特质 启用打印的 Display 特质时,才实现 cmp_display 方法。

Filename: 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);
        }
    }
}
Listing 10-15: Conditionally implementing methods on a generic type depending on trait bounds

我们还可以为实现另一个特质的任何类型有条件地实现一个特质。在满足特质边界条件的任何类型上实现特质被称为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();
}

毯式实现出现在特征文档的“实现者”部分。

特质和特质边界让我们可以使用泛型类型参数编写代码,以减少重复,但同时指定我们希望泛型类型具有特定的行为。编译器可以使用特质边界信息来检查所有使用我们代码的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个类型未定义的方法,我们会在运行时得到错误。但是 Rust 将这些错误移到了编译时,因此我们必须在代码能够运行之前修复这些问题。此外,我们不必编写在运行时检查行为的代码,因为我们在编译时已经检查过了。这样做在不放弃泛型灵活性的情况下提高了性能。