Trait:定义共享行为

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

注意:Trait 类似于其他语言中常称为 接口 的特性,尽管有一些差异。

定义 Trait

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

例如,假设我们有多个结构体,它们持有不同种类和数量的文本:一个 NewsArticle 结构体持有特定位置的新闻故事,以及一个 SocialPost 结构体最多可以包含 280 个字符,以及指示它是新帖子、转发还是对另一个帖子的回复的元数据。

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

文件名: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
列表 10-12: 一个 Summary trait,包含由 summarize 方法提供的行为

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

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

一个 trait 的主体中可以有多个方法:方法签名每行列出一个,并且每行以分号结束。

在类型上实现 Trait

现在我们已经定义了 Summary trait 方法的所需签名,我们可以在媒体聚合器中的类型上实现它。列表 10-13 展示了在 NewsArticle 结构体上实现 Summary trait 的示例,该实现使用标题、作者和位置来创建 summarize 的返回值。对于 SocialPost 结构体,我们将 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
列表 10-13: 在 NewsArticleSocialPost 类型上实现 Summary trait

在类型上实现 trait 与实现普通方法类似。不同之处在于,在 impl 之后,我们放置要实现的 trait 名称,然后使用 for 关键字,然后指定要为其实现 trait 的类型的名称。在 impl 块内,我们放置 trait 定义中定义的方法签名。我们不使用分号,而是使用花括号并在方法体中填充我们希望该 trait 的方法对于特定类型具有的具体行为。

现在库已经在 NewsArticleSocialPost 上实现了 Summary trait,crate 的用户可以像调用普通方法一样在 NewsArticleSocialPost 的实例上调用 trait 方法。唯一的区别是用户也必须将 trait 和类型一起引入作用域。以下是一个二进制 crate 如何使用我们的 aggregator 库 crate 的示例:

use aggregator::{SocialPost, Summary};

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

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

此代码会打印 1 new post: horse_ebooks: of course, as you probably already know, people

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

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

默认实现

有时,为 trait 中的部分或全部方法提供默认行为是很有用的,而不是要求在每种类型上实现所有方法。然后,当我们在特定类型上实现 trait 时,我们可以保留或覆盖每个方法的默认行为。

在列表 10-14 中,我们为 Summary trait 的 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
列表 10-14: 定义一个具有 summarize 方法默认实现的 Summary trait

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

尽管我们不再直接在 NewsArticle 上定义 summarize 方法,但我们提供了一个默认实现并指定 NewsArticle 实现了 Summary trait。因此,我们仍然可以在 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 中 SocialPostSummary 的实现。原因是覆盖默认实现的语法与实现没有默认实现的 trait 方法的语法相同。

默认实现可以调用同一 trait 中的其他方法,即使这些其他方法没有默认实现。通过这种方式,trait 可以提供许多有用的功能,并且只要求实现者指定其中一小部分。例如,我们可以定义 Summary trait,使其具有一个要求实现的 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

要使用此版本的 Summary,我们只需在类型上实现该 trait 时定义 summarize_author 即可:

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

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

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

定义 summarize_author 后,我们可以在 SocialPost 结构体的实例上调用 summarize,并且 summarize 的默认实现将调用我们提供的 summarize_author 定义。因为我们已经实现了 summarize_author,所以 Summary trait 赋予了我们 summarize 方法的行为,而无需我们再编写任何代码。示例如下:

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

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

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

此代码会打印 1 new post: (Read more from @horse_ebooks...)

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

Trait 作为参数

现在你已经了解如何定义和实现 trait,我们可以探索如何使用 trait 来定义接受多种不同类型的函数。我们将使用在列表 10-13 中对 NewsArticleSocialPost 类型实现的 Summary trait 来定义一个 notify 函数,该函数在其 item 参数上调用 summarize 方法,该参数是实现 Summary trait 的某种类型。为此,我们使用 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

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

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

Trait 约束语法

impl Trait 语法适用于简单情况,但实际上是称为 trait 约束 的更长形式的语法糖;如下所示:

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

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

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

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

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

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

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

使用 + 语法指定多个 Trait 约束

我们还可以指定多个 trait 约束。假设我们希望 notifyitem 上使用显示格式化(Display)以及 summarize:我们在 notify 定义中指定 item 必须同时实现 DisplaySummary。我们可以使用 + 语法做到这一点:

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

+ 语法对于泛型类型上的 trait 约束也有效:

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

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

使用 where 子句使 Trait 约束更清晰

使用过多的 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!()
}

这个函数签名不那么杂乱:函数名称、参数列表和返回类型紧密相连,类似于没有大量 trait 约束的函数。

返回实现 Trait 的类型

我们还可以在返回位置使用 impl Trait 语法来返回实现 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

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

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

仅通过 trait 来指定返回类型的能力在闭包(closure)和迭代器(iterator)的上下文中特别有用,这将在第 13 章中介绍。闭包和迭代器创建的类型要么只有编译器知道,要么类型签名非常长。impl Trait 语法使你可以简洁地指定函数返回实现 Iterator trait 的某种类型,而无需写出非常长的类型签名。

但是,只有在返回单一类型时才能使用 impl Trait。例如,以下代码试图返回 NewsArticleSocialPost,并将返回类型指定为 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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    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 {
        SocialPost {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            repost: false,
        }
    }
}

由于 impl Trait 语法在编译器中的实现方式存在限制,不允许返回 NewsArticleSocialPost(或其他不同类型)。我们将在“使用 Trait Object 允许不同类型的值”部分介绍如何编写具有此行为的函数。

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

通过将 trait 约束与使用泛型类型参数的 impl 块结合使用,我们可以有条件地为实现指定 trait 的类型实现方法。例如,列表 10-15 中的类型 Pair<T> 总是实现 new 函数来返回 Pair<T> 的新实例(回想第 5 章的“定义方法”部分,Selfimpl 块类型的类型别名,在本例中是 Pair<T>)。但在下一个 impl 块中,Pair<T> 仅当其内部类型 T 实现了启用比较的 PartialOrd trait 以及 启用打印的 Display trait 时,才实现 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: 根据 Trait 约束有条件地在泛型类型上实现方法

我们还可以为任何实现了另一个 trait 的类型有条件地实现一个 trait。在一个满足 trait 约束的任何类型上的 trait 实现称为 全覆盖实现(blanket implementation),并在 Rust 标准库中广泛使用。例如,标准库为任何实现 Display trait 的类型实现了 ToString trait。标准库中的 impl 块看起来类似于以下代码:

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

因为标准库具有此全覆盖实现,所以我们可以在任何实现了 Display trait 的类型上调用由 ToString trait 定义的 to_string 方法。例如,我们可以像这样将整数转换为相应的 String 值,因为整数实现了 Display trait:

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

全覆盖实现出现在 trait 文档的“实现者”(Implementors)部分。

Trait 和 trait 约束使我们可以编写使用泛型类型参数的代码,以减少重复,同时还向编译器指定我们希望泛型类型具有特定行为。然后,编译器可以使用 trait 约束信息来检查与我们代码一起使用的所有具体类型是否提供了正确的行为。在动态类型语言中,如果在没有定义方法的类型上调用方法,我们将在运行时获得错误。但 Rust 将这些错误移至编译时,因此我们在代码运行之前就被迫修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做可以在不放弃泛型灵活性的情况下提高性能。