特性:定义共享行为

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

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

定义特性

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

例如,假设我们有多个结构体,用于保存各种类型和数量的文本:一个 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 显示了在 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 特性,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 的实例,我们指定一个空的 impl 代码块,其中包含 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 中 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 结构体的实例上调用 summarize,并且 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...)

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

特性作为参数

现在您已经了解了如何定义和实现特性,我们可以探索如何使用特性来定义接受许多不同类型的函数。我们将使用我们在列表 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 的参数。使用 impl Trait 语法这样做看起来像这样

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

如果我们希望此函数允许 item1item2 具有不同的类型(只要这两种类型都实现 Summary),则使用 impl Trait 是合适的。但是,如果我们想强制两个参数具有相同的类型,则必须使用特性约束,如下所示

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

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

使用 + 语法指定多个特性约束

我们还可以指定多个特性约束。假设我们希望 notifyitem 上使用显示格式以及 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 子句内指定特性约束的替代语法。因此,我们可以使用 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!() }

此函数的签名不太混乱:函数名称、参数列表和返回类型紧密地结合在一起,类似于没有大量特性约束的函数。

返回实现特性的类型

我们还可以在返回位置使用 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, } }

通过对返回类型使用 impl Summary,我们指定 returns_summarizable 函数返回某种实现 Summary 特性的类型,而无需命名具体类型。在本例中,returns_summarizable 返回 Tweet,但是调用此函数的代码不需要知道这一点。

仅通过它实现的特性来指定返回类型的功能在闭包和迭代器的上下文中特别有用,我们将在第 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, } } }

由于围绕编译器中 impl Trait 语法的实现方式的限制,不允许返回 NewsArticleTweet 之一。我们将在第 18 章的 “使用允许不同类型值的特性对象”部分介绍如何编写具有此行为的函数。

使用特性约束有条件地实现方法

通过将特性约束与使用泛型类型参数的 impl 代码块一起使用,我们可以为实现指定特性的类型有条件地实现方法。例如,列表 10-15 中的类型 Pair<T> 始终实现 new 函数以返回 Pair<T> 的新实例(回想一下第 5 章的 “定义方法”部分,Selfimpl 代码块类型的类型别名,在本例中为 Pair<T>)。但是在下一个 impl 代码块中,只有当其内部类型 T 实现启用比较的 PartialOrd 特性和启用打印的 Display 特性时,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:根据特性约束有条件地在泛型类型上实现方法

我们还可以有条件地为任何实现另一个特性的类型实现特性。在任何满足特性约束的类型上实现特性都称为覆盖实现,并在 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(); }

覆盖实现出现在特性的文档的“Implementors”部分中。

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