特征:定义共享行为

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

注意: 特征类似于其他 语言,尽管存在一些差异。

定义特征

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

例如,假设我们有多个结构体,它们包含各种 文本数量:一个NewsArticle结构体,该结构体包含以 特定位置和Tweet最多可以有 280 个字符 带有元数据,指示它是新推文、转推还是回复 到另一条推文。

我们想制作一个名为aggregator那可以 显示可能存储在NewsArticleTweet实例。为此,我们需要每种类型的摘要,并且我们将请求该摘要 summary 通过调用summarize方法。示例 10-12 显示了 公共Summarytrait 来表达这种行为。

文件名: src/lib.rs

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

示例 10-12: ASummarytrait 的 trait 中,该 trait 由 行为summarize方法

在这里,我们使用traitkeyword 的 Keyword 中,然后是 trait 的名称 哪个是Summary在这种情况下。我们还将 trait 声明为pub因此 依赖于这个 crate 的 crate 也可以使用这个特性,正如我们将在 几个例子。在大括号内,我们声明方法签名 ,它描述了实现此 trait 的类型的行为,其中在 这种情况是fn summarize(&self) -> String.

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

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

在 Type 上实现 trait

现在我们已经定义了Summarytrait 的方法、 我们可以在 Media Aggregator 中的类型上实现它。示例 10-13 显示 的SummarytraitNewsArticlestruct 的 标题、作者和位置 创建返回值summarize.对于Tweetstruct 中,我们定义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:实现SummarytraitNewsArticleTweet类型

在类型上实现 trait 类似于实现常规方法。这 不同的是,在impl,我们输入我们想要实现的 trait 名称, 然后使用forkeyword,然后指定我们想要的类型的名称 实现 trait for。在impl块中,我们将方法签名 特征定义已定义。而不是在每个 签名,我们使用大括号,并在方法主体中填充特定的 行为。

现在,该库已经实现了Summarytrait 开启NewsArticleTweet中,crate 的用户可以在NewsArticleTweet以同样的方式调用 regular 方法。唯一的 区别在于,用户必须将 trait 引入 scope 中,并且 类型。下面是一个二进制 crate 如何使用我们的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 new tweet: horse_ebooks: of course, as you probably already know, people.

其他依赖于aggregatorcrate 也可以带来Summarytrait 放入 scope 中实现Summary在他们自己的类型上。一个限制 请注意,只有当 trait 或 type 和/或 both 都是我们 crate 的本地。例如,我们可以实现 standard 库特征,如Display在自定义类型(如Tweet作为我们aggregatorcrate 功能,因为Tweet是我们aggregator板条箱。我们还可以实施SummaryVec<T>在我们的aggregatorcrate 的 traitSummary是我们aggregator板条箱。

但是我们不能在外部类型上实现 external trait。例如,我们不能 实现Displaytrait 开启Vec<T>在我们的aggregatorcrate,因为DisplayVec<T>都在 standard 库 中定义,而不是 本地到我们的aggregator板条箱。此限制是称为 coherence 的属性的一部分,更具体地说是孤立规则,之所以这样命名,是因为 父类型不存在。此规则确保其他人的代码不能 破解你的代码,反之亦然。如果没有该规则,两个 crate 可以实现 相同的 trait 对应相同的类型,并且 Rust 不知道哪个实现 使用。

默认实施

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

在示例 10-14 中,我们为summarize方法Summarytrait 而不是只定义方法签名,就像我们在 示例 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:定义Summarytrait 替换为默认的 实现summarize方法

要使用默认实现来汇总NewsArticle我们 指定一个空的implblock 替换为impl Summary for NewsArticle {}.

即使我们不再定义summarizemethod 开启NewsArticle直接,我们提供了一个默认实现并指定了NewsArticle实现Summary特性。因此,我们仍然可以调用 这summarizemethod 在NewsArticle喜欢这个:

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...).

创建默认实现不需要我们更改任何内容 实施SummaryTweet在示例 10-13 中。原因是 覆盖默认实现的语法与语法相同 实现没有默认实现的 trait 方法。

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

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,我们可以调用summarizeTweetstruct 和summarize将调用 定义summarize_author我们提供的服务。因为我们已经实施了summarize_authorSummarytrait 为我们提供了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 来定义接受许多不同类型的函数。我们将使用Summarytrait 中,我们在NewsArticleTweet键入 示例 10-13 定义一个notify函数调用summarize方法 在其item参数,该参数是实现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参数中,我们指定implkeyword 和特征名称。此参数接受实现 指定的 trait 中。在 bodynotify,我们可以调用item这些来自Summarytrait 的summarize.我们可以调用notify并传入NewsArticleTweet.调用 函数替换为任何其他类型的Stringi32,不会编译 因为这些类型没有实现Summary.

特征绑定语法

impl Trait语法适用于简单的情况,但实际上是语法 糖是一种较长的形式,称为性状绑定;它看起来像这样:

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

这种较长的形式等效于上一节中的示例,但 更冗长。我们使用泛型类型的声明放置 trait bounds parameter 的 Parameter 的 S 参数。

impl Trait语法很方便,并且使代码更简洁 case 中,而 fuller trait bound 语法可以在其他 例。例如,我们可以有两个参数来实现Summary.行为 因此,使用impl Trait语法如下所示:

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

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

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

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

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

我们还可以指定多个 trait bound。说我们想要notify使用 显示格式以及summarizeitem:我们在notify定义item必须同时实现DisplaySummary.我们能做到 所以使用语法:+

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

该语法也适用于泛型类型的 trait bounds:+

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

指定两个 trait bounds 后,主体notify可以调用summarize并用于格式化{}item.

更清晰的 trait boundswhere

使用过多的 trait bounds 有其缺点。每个泛型都有自己的 trait bounds,因此具有多个泛型类型参数的函数可以包含大量 trait 绑定信息, 使函数签名难以阅读。因此,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!()
}

这个函数的签名不那么杂乱:函数名称、参数列表、 和 return 类型靠得很近,类似于没有很多 trait 的函数 bounds 的

返回实现 trait 的类型

我们还可以使用impl Trait语法以返回 value 实现 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函数返回一些实现Summarytrait 中,而不命名具体类型。在这种情况下,returns_summarizable返回Tweet,但调用此函数的代码不需要知道这一点。

仅通过它实现的 trait 指定返回类型的能力是 在闭包和迭代器的上下文中特别有用,我们将在 第 13 章.闭包和迭代器创建只有编译器知道的类型或 类型。这impl Trait语法让您简洁 指定函数返回实现Iterator特性 而无需写出很长的类型。

但是,您只能使用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,
        }
    }
}

返回 aNewsArticleTweet由于限制而不允许 围绕impl Trait语法在编译器中实现。我们将涵盖 如何在“Using trait Objects that 允许不同的值 Types“部分。

使用 trait bounds 有条件地实现方法

通过使用与impl使用泛型类型参数的块, 我们可以为实现指定 性状。例如,类型Pair<T>在示例 10-15 中,始终实现new函数返回Pair<T>(回想一下第 5 章的“定义方法”部分,Selfimpl块,在本例中为Pair<T>).但在接下来的implPair<T>仅实现cmp_displaymethod (如果其内部类型)T实现PartialOrd特性 ,这样就可以进行比较,并且Display支持打印的 trait 进行打印。

文件名: 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 边界的 generic 类型

我们还可以有条件地为任何实现 另一个特征。在满足 trait 的任何类型的 trait 上的实现 bounds 称为 blanket implementations,在 Rust 标准库。例如,标准库实现了ToStringtrait 的Display特性。这impl块的外观类似于以下代码:

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

因为标准库具有这种一揽子实现,所以我们可以调用to_string方法由ToStringtrait 的 trait 这Display特性。例如,我们可以将整数转换为它们对应的String值,因为整数实现Display:

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

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

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准