特性:定义共享行为

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

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

定义特性

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

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

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

文件名:src/lib.rs

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

清单 10-12:一个 Summary 特性,包含由 summarize 方法提供的行为

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

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

一个特性可以在其主体中包含多个方法:方法签名按行排列,每行以分号结尾。

在类型上实现特性

现在我们已经定义了 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:在 NewsArticleTweet 类型上实现 Summary 特性

在类型上实现特性类似于实现常规方法。区别在于在 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: of course, as you probably already know, people

依赖于 aggregator crate 的其他 crate 也可以将 Summary 特性引入作用域,以便在它们自己的类型上实现 Summary。需要注意的一个限制是,只有当特性或类型或两者都是我们 crate 本地时,我们才能在类型上实现特性。例如,我们可以将标准库特性(如 Display)在自定义类型(如 Tweet)上实现,作为我们 aggregator crate 功能的一部分,因为类型 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 中那样仅定义方法签名。

文件名: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 特性

要使用默认实现来汇总 NewsArticle 的实例,我们指定一个空的 implimpl 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());
}

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

创建默认实现不需要我们更改清单 10-13 中 SummaryTweet 上的实现。原因是覆盖默认实现的语法与实现没有默认实现的特性方法的语法相同。

默认实现可以调用同一特性中的其他方法,即使这些其他方法没有默认实现。通过这种方式,特性可以提供许多有用的功能,并且只需要实现者指定其中的一小部分。例如,我们可以定义 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 结构体的实例上调用 summarizesummarize 的默认实现将调用我们提供的 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...)

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

作为参数的特性

现在您已经了解了如何定义和实现特性,我们可以探索如何使用特性来定义接受许多不同类型的函数。我们将使用我们在清单 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 特性的 item 的任何方法,例如 summarize。我们可以调用 notify 并传入 NewsArticleTweet 的任何实例。调用带有任何其他类型(如 Stringi32)的函数的代码将无法编译,因为这些类型未实现 Summary

特性边界语法

impl Trait 语法适用于简单的情况,但实际上是称为特性边界的较长形式的语法糖;它看起来像这样

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

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

impl Trait 语法很方便,可以在简单情况下使代码更简洁,而更完整的特性边界语法可以在其他情况下表达更多的复杂性。例如,我们可以有两个实现 Summary 的参数。如果我们要允许 item1item2 具有不同的类型(只要两种类型都实现 Summary),则使用 impl Trait 的方法如下所示

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

如果我们想强制两个参数具有相同的类型,则必须使用特性边界,如下所示

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

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

使用 + 语法指定多个特性边界

我们还可以指定多个特性边界。假设我们希望 notify 使用显示格式以及 item 上的 summarize:我们在 notify 定义中指定 item 必须同时实现 DisplaySummary。我们可以使用 + 语法来实现

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

+ 语法对于泛型类型上的特性边界也有效

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

指定两个特性边界后,notify 的主体可以调用 summarize 并使用 {} 来格式化 item

使用 where 子句更清晰地表达特性边界

使用过多的特性边界也有其缺点。每个泛型都有其自己的特性边界,因此具有多个泛型类型参数的函数可以在函数的名称和其参数列表之间包含大量特性边界信息,这使得函数签名难以阅读。因此,Rust 提供了一种替代语法,用于在函数签名之后的 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!()
}

这个函数的签名更加简洁:函数名、参数列表和返回类型都紧密地排列在一起,类似于没有大量 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。例如,这段代码返回 NewsArticleTweet,并把返回类型指定为 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 语法的限制,不允许返回 NewsArticleTweet 中的任意一个。我们将在第 17 章的 “使用允许不同类型值的 Trait 对象” 部分介绍如何编写具有此行为的函数。第 17 章的内容。

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

通过在使用了泛型类型参数的 impl 块中使用 trait 约束,我们可以为实现了指定 trait 的类型有条件地实现方法。例如,清单 10-15 中的 Pair<T> 类型总是实现 new 函数来返回一个新的 Pair<T> 实例(回顾第 5 章的“定义方法”部分,Selfimpl 块的类型别名,在这种情况下是 Pair<T>)。但是在下一个 impl 块中,只有当它的内部类型 T 实现了支持比较的 PartialOrd trait 并且 实现了支持打印的 Display trait 时,Pair<T> 才实现 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 被称为覆盖实现,并在 Rust 标准库中广泛使用。例如,标准库在任何实现了 Display trait 的类型上实现了 ToString trait。标准库中的 impl 块看起来类似于以下代码:

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

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

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

覆盖实现在 trait 的文档的“Implementors”部分中显示。

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