特征:定义共享行为


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


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


定义特征


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


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


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


文件名: src/lib.rs

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


示例 10-12:一个 Summary trait,由 summarize 方法提供的行为组成


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


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


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


在 Type 上实现 trait


现在我们已经定义了 Summary 特征方法的所需签名,我们可以在 media 聚合器中的类型上实现它。示例 10-13 显示了 NewsArticle 结构体上 Summary trait 的实现,它使用 标题、作者和位置 创建返回值 总结。对于 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 类型


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


现在,库已在 NewsArticle Tweet 中,crate 的用户可以在 NewsArticleTweet 的调用方式与我们称为常规方法的方式相同。唯一的区别是用户必须将 trait 和类型引入范围。下面是一个二进制 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 trait 添加到作用域中,以便在自己的类型上实现 Summary。需要注意的一个限制是,只有当 trait 或类型,或两者都是 crate 的本地时,我们才能在类型上实现 trait。例如,我们可以在自定义类型(如 Tweet)上实现标准库特征(如 Display),作为 aggregator crate 功能,因为 Tweet 类型是我们的 aggregator crate 中。我们还可以在 Vec<T> 上实现 Summary aggregator crate,因为 trait Summary 是聚合的本地 板条箱。


但是我们不能在 external 类型上实现 external traits。例如,我们无法在聚合器 crate 中的 Vec<T> 上实现 Display 特征,因为 DisplayVec<T> 都是在标准库中定义的,并且不是我们的聚合器 crate 的本地函数。此限制是名为 coherence 的 coherence 规则,更具体地说是 orphan rule,之所以这样命名,是因为父类型不存在。此规则确保其他人的代码不会破坏您的代码,反之亦然。如果没有这条规则,两个 crate 可以为同一类型实现相同的 trait,而 Rust 将不知道该使用哪个实现。


默认实施


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


在示例 10-14 中,我们为 summary trait 而不是像示例 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 {}


即使我们不再在 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 中 Summary on Tweet 的实现的任何内容。原因是覆盖默认实现的语法与实现没有默认实现的 trait 方法的语法相同。


默认实现可以调用同一 trait 中的其他方法,即使这些其他方法没有默认实现。通过这种方式,一个 trait 可以提供很多有用的功能,并且只需要实现者指定它的一小部分。例如,我们可以将 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 当我们在一个类型上实现 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 后,我们可以对 Tweet 结构体和 summarize 的默认实现将调用我们提供的 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...) .


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


作为参数的特征


现在,您知道如何定义和实施特征,我们可以探索如何使用 trait 来定义接受许多不同类型的函数。我们将使用 我们在示例 10-13 中的 NewsArticleTweet 类型上实现的 Summary trait,以定义一个 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());
}


我们指定 impl keyword 和特征名称。此参数接受实现 指定的 trait 中。在 notify 的主体中,我们可以对 item 调用任何方法 ,这些特征来自 Summary 特征,例如 summarize。我们可以调用 notify 并传入 NewsArticleTweet 的任何实例。使用任何其他类型(例如 Stringi32)调用函数的代码不会编译,因为这些类型不实现 Summary


特征绑定语法


impl Trait 语法适用于简单的情况,但实际上是较长形式(称为 trait bound)的语法糖;它看起来像这样:

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


这种较长的形式等效于上一节中的示例,但更详细。我们将 trait bounds 与泛型类型参数的声明放在冒号后面和尖括号内。


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

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


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

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


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


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


我们还可以指定多个 trait bound。假设我们希望 notify 使用显示格式以及对 item 进行汇总:我们在 notify 中指定 定义该项目必须同时实现 DisplaySummary。我们可以使用 + 语法来做到这一点:

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


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

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


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


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


使用过多的 trait bounds 有其缺点。每个泛型都有自己的 trait bounds,因此具有多个泛型类型参数的函数可以在函数名称和其参数列表之间包含大量 trait bound信息,从而使函数签名难以阅读。出于这个原因,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 的类型


我们还可以在 return 位置使用 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 SummaryNewsArticleTweet 的以下代码将不起作用:

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


不允许返回 NewsArticleTweet,因为在编译器中实现 impl Trait 语法的方式存在限制。我们将在“使用 trait 对象 允许不同的值 Types“部分。


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


通过使用使用泛型类型参数的 impl 块绑定的 trait,我们可以有条件地为实现指定 trait 的类型实现方法。例如,示例 10-15 中的类型 Pair<T> 始终实现 new 函数返回 Pair<T> 的新实例(回想一下 第 5 章的“定义方法”部分 Selfimpl 块类型的类型别名,在本例中为 对<T>)。但在下一个 impl 块中,Pair<T> 只实现了 cmp_display 方法,如果其内部类型 T 实现了启用比较的 PartialOrd trait 启用打印的 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 边界在泛型类型上有条件地实现方法


我们还可以有条件地为实现另一个 trait 的任何类型的 trait 实现一个 trait。满足 trait 边界的任何类型的 trait 实现称为 blanket implementations,在 Rust 标准库。例如,标准库实现了 ToString trait 应用于实现 Display trait 的任何类型。impl 块的外观类似于以下代码:

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


因为标准库具有这种一揽子实现,所以我们可以调用 to_string ToString trait 在实现 Display trait 的任何类型上定义的方法。例如,我们可以将整数转换为它们对应的 字符串值,因为整数实现 Display

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


一揽子实现出现在 trait 文档中的 “Implementors” 部分。


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