特征:定义共享行为

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

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

定义特征

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

例如,假设我们有多个结构体,它们包含各种类型和数量的文本:一个 NewsArticle 结构体,其中包含在特定位置归档的新闻报道,以及一个 Tweet 结构体,其中最多可以包含 280 个字符以及指示它是新推文、转发还是回复另一条推文的元数据。

我们想创建一个名为 aggregator 的媒体聚合器库箱,它可以显示可能存储在 NewsArticleTweet 实例中的数据的摘要。为此,我们需要每个类型提供一个摘要,我们将通过调用实例上的 summarize 方法来请求该摘要。清单 10-12 显示了表达此行为的公共 Summary 特征的定义。

文件名:src/lib.rs

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

清单 10-12:由 summarize 方法提供的行为组成的 Summary 特征

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

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

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

在类型上实现特征

现在我们已经定义了 Summary 特征的方法的所需签名,我们可以在媒体聚合器中的类型上实现它。清单 10-13 显示了在 NewsArticle 结构体上实现 Summary 特征,该结构体使用标题、作者和位置来创建 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:在 NewsArticleTweet 类型上实现 Summary 特征

在类型上实现特征类似于实现常规方法。区别在于,在 impl 之后,我们放置要实现的特征名称,然后使用 for 关键字,然后指定要为其实现特征的类型的名称。在 impl 块中,我们放置了特征定义已定义的方法签名。我们没有在每个签名后添加分号,而是使用大括号并用我们希望特征的方法针对特定类型具有的特定行为来填充方法体。

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

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 条新推文:horse_ebooks:当然,您可能已经知道,人们

依赖于 aggregator crate 的其他 crate 也可以将 Summary trait 引入作用域,以便在其自己的类型上实现 Summary。需要注意的一个限制是,我们只能在类型上实现 trait,如果该 trait 或类型至少有一个是本地 crate 中的。例如,我们可以将标准库 trait(如 Display)实现为 aggregator crate 功能的一部分,例如 Tweet,因为类型 Tweet 是我们 aggregator crate 本地的。我们还可以在 aggregator crate 中对 Vec<T> 实现 Summary,因为 trait Summary 是我们 aggregator crate 本地的。

但是我们不能在外部类型上实现外部 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 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 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 中 TweetSummary 的实现。原因是,覆盖默认实现的语法与实现没有默认实现的 trait 方法的语法相同。

默认实现可以调用同一 trait 中的其他方法,即使这些其他方法没有默认实现。这样,trait 可以提供许多有用的功能,并且仅要求实现者指定其中的一小部分。例如,我们可以将 Summary trait 定义为具有需要实现的 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,我们只需要在对类型实现 trait 时定义 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 trait 为我们提供了 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...)

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

作为参数的 Trait

现在您已经知道了如何定义和实现 trait,我们可以探索如何使用 trait 来定义接受多种不同类型的函数。我们将使用在代码清单 10-13 中对 NewsArticleTweet 类型实现的 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 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());
}

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

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 绑定。假设我们希望 notify 使用显示格式以及 item 上的 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 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 trait 的某种类型,而无需命名具体类型。在这种情况下,returns_summarizable 返回一个 Tweet,但是调用此函数的代码不需要知道这一点。

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

但是,如果只返回一种类型,则只能使用 impl Trait。例如,以下代码使用指定为 impl Summary 的返回类型返回 NewsArticleTweet 将不起作用

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 语法的实现方式存在限制,因此不允许返回 NewsArticleTweet。我们将在第 17 章的“使用允许不同类型值的 Trait 对象”一节中介绍如何编写具有此行为的函数。

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

通过在使用泛型类型参数的 impl 块中使用特征边界,我们可以有条件地为实现指定特征的类型实现方法。例如,清单 10-15 中的类型 Pair<T> 始终实现 new 函数以返回 Pair<T> 的新实例(回想一下第 5 章““定义方法”部分,Selfimpl 块类型的类型别名,在本例中为 Pair<T>)。但在下一个 impl 块中,Pair<T> 仅在其内部类型 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:根据特征边界有条件地在泛型类型上实现方法

我们还可以为实现另一个特征的任何类型有条件地实现一个特征。对满足特征边界的任何类型的特征实现称为覆盖实现,并在 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 将这些错误转移到编译时,因此我们不得不在代码能够运行之前修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做在不放弃泛型的灵活性的情况下提高了性能。