特征:定义共享行为

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

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

定义特征

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

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

我们想制作一个名为 显示可能存储在 OR 实例中的数据的摘要。为此,我们需要每种类型的摘要,并且我们将请求该摘要 summary 通过对实例调用方法。示例 10-12 显示了 表达此行为的公共特征的定义。aggregatorNewsArticleTweetsummarizeSummary

文件名: src/lib.rs

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

示例 10-12:一个 Summary trait,它由 Summarize 方法提供的行为

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

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

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

在 Type 上实现 trait

现在我们已经定义了 trait 方法的所需签名, 我们可以在 Media Aggregator 中的类型上实现它。示例 10-13 显示 结构体上 trait 的实现,它使用 标题、作者和位置 创建返回值 .对于结构体,我们定义为用户名 后跟推文的整个文本,假设推文内容为 已限制为 280 个字符。SummarySummaryNewsArticlesummarizeTweetsummarize

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

在类型上实现 trait 类似于实现常规方法。这 区别在于,在 之后,我们放置了我们想要实现的 trait 名称, 然后使用关键字,然后指定我们想要的类型的名称 实现 trait for。在区块中,我们将方法签名 特征定义已定义。而不是在每个 签名,我们使用大括号,并在方法主体中填充特定的 行为。implforimpl

现在,库已经在 和 上实现了 trait ,crate 的用户可以在 和 的实例上调用 trait 方法,就像我们调用常规方法一样。唯一的 区别在于,用户必须将 trait 引入 scope 中,并且 类型。下面是一个二进制 crate 如何使用我们的库 crate 的示例:SummaryNewsArticleTweetNewsArticleTweetaggregator

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

其他依赖于 crate 的 crate 也可以将 trait 引入范围,以便在它们自己的类型上实现。一个限制 请注意,只有当 trait 或 type 和/或 both 都是我们 crate 的本地。例如,我们可以实现 standard library trait 一样,就像在自定义类型上一样,作为我们 crate 功能的一部分,因为该类型是 crate 的本地类型。我们也可以在我们的 crate 中实现 on,因为这个 trait 是我们的 crate 本地的。aggregatorSummarySummaryDisplayTweetaggregatorTweetaggregatorSummaryVec<T>aggregatorSummaryaggregator

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

默认实施

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

在示例 10-14 中,我们为 trait 的方法指定了一个默认字符串,而不是像在 示例 10-12.summarizeSummary

文件名: 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:使用 default 定义 Summary trait summarize 方法的实现

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

即使我们不再直接定义方法,我们也提供了一个默认实现并指定了实现 trait 的 trait。因此,我们仍然可以调用 的方法,如下所示:summarizeNewsArticleNewsArticleSummarysummarizeNewsArticle

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 中 on 的实现。原因是 覆盖默认实现的语法与语法相同 实现没有默认实现的 trait 方法。SummaryTweet

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

要使用这个版本的 ,我们只需要定义何时在类型上实现 trait:Summarysummarize_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)
    }
}

定义 后,我们可以调用结构体的实例,而 的默认实现将调用 定义。因为我们已经实现了 ,所以 trait 为我们提供了方法的行为,而无需我们编写更多代码。具体内容如下 如下所示:summarize_authorsummarizeTweetsummarizesummarize_authorsummarize_authorSummarysummarize

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 示例 10-13 定义一个调用 在其参数上,该参数是实现 trait 的某种类型。为此,我们使用语法,如下所示:SummaryNewsArticleTweetnotifysummarizeitemSummaryimpl 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());
}

我们指定 keyword 和 trait name 不是参数的具体类型。此参数接受实现 指定的 trait 中。在 的主体中,我们可以调用来自 trait 的任何方法,例如 。我们可以调用并传入 或 的任何实例。调用 具有任何其他类型(例如 a 或 an )的函数都不会编译 因为这些类型不实现 .itemimplnotifyitemSummarysummarizenotifyNewsArticleTweetStringi32Summary

特征绑定语法

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

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

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

语法很方便,使代码更简洁 case 中,而 fuller trait bound 语法可以在其他 例。例如,我们可以有两个实现 .行为 所以语法看起来像这样:impl TraitSummaryimpl Trait

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

如果我们希望这个函数允许并具有不同的类型(只要两种类型都实现 ),那么 using 是合适的。如果 我们希望强制两个参数具有相同的类型,但是,我们必须使用 trait 绑定,如下所示:impl Traititem1item2Summary

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

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

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

我们还可以指定多个 trait bound。假设我们想使用 显示格式以及 ON :我们在定义中指定必须同时实现 和 。我们能做到 所以使用语法:notifysummarizeitemnotifyitemDisplaySummary+

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

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

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

指定两个 trait bounds 后,主体可以调用并使用 format .notifysummarize{}item

使用 where 子句的更清晰的 trait Bounds

使用过多的 trait bounds 有其缺点。每个泛型都有自己的 trait bounds,因此具有多个泛型类型参数的函数可以包含大量 trait 绑定信息, 使函数签名难以阅读。因此,Rust 有替代的 在函数后的子句中指定 trait bounds 的语法 签名。所以,与其这样写: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 的类型

我们还可以在 return 位置使用语法来返回一个 value 实现 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)
    }
}

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,
    }
}

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

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

但是,只有在返回单个类型时才能使用。为 示例中,此代码返回 a 或 a 以及 return 类型指定为 WILL 不起作用:impl TraitNewsArticleTweetimpl 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,
        }
    }
}

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

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

通过使用与使用泛型类型参数的块绑定的 trait, 我们可以为实现指定 性状。例如,示例 10-15 中的类型总是实现函数以返回一个新的实例(回想一下第 5 章的“定义方法”部分,它是块类型的类型别名,在本例中为 )。但是在下一个块中,仅当其内部类型实现 trait 时,才实现该方法 ,这支持比较,而 trait 则支持打印。implPair<T>newPair<T>SelfimplPair<T>implPair<T>cmp_displayTPartialOrdDisplay

文件名: 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 标准库。例如,标准库在实现 trait 的任何类型上实现 trait。标准库中的块类似于以下代码:ToStringDisplayimpl

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

因为标准库有这个 blanket 实现,所以我们可以在任何实现 性状。例如,我们可以像这样将整数转换为它们相应的值,因为整数实现了 :to_stringToStringDisplayStringDisplay

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

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

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

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