特质:定义共享行为
一个 trait 定义了特定类型具有的功能以及可以与其他类型共享的功能。我们可以使用 trait 以抽象的方式定义共享行为。我们可以使用 trait bounds 来指定泛型类型可以是具有某些行为的任何类型。
注意:Trait 与其它语言中经常称为 接口 的功能类似,尽管有一些不同。
定义一个特质
一个类型的行为主由我们可以调用该类型的方法组成。如果我们可以对所有这些类型调用相同的方法,那么不同的类型就共享相同的行为。特征定义是一种将方法签名组合在一起的方式,以定义实现某个目的所需的一组行为。
例如,假设我们有多个结构体,它们持有各种类型和数量的文本:NewsArticle
结构体保存在特定地点发布的一篇新闻故事,以及 Tweet
结构体,最多可以包含 280 个字符,以及指示它是新推文、转推还是回复其他推文的元数据。
我们想要制作一个名为 aggregator
的媒体聚合库 crate,可以显示可能存储在 NewsArticle
或 Tweet
实例中的数据摘要。为此,我们需要每种类型的摘要,并将通过在实例上调用 summarize
方法来请求该摘要。列表 10-12 显示了一个公共 Summary
特性的定义,该特性表达了这种行为。
这里,我们使用 trait
关键字和 trait 的名称来声明一个 trait,这个例子中是 Summary
。我们还声明 trait 为 pub
,以便依赖此 crate 的其他 crate 也可以使用此 trait,我们将在几个示例中看到。在大括号内,我们声明了描述实现此 trait 的类型的行为的方法签名,这里为 fn summarize(&self) -> String
。
在方法签名之后,我们使用分号而不是在大括号内提供实现。每个实现此特征的类型都必须为其方法体提供自己的自定义行为。编译器将强制任何具有 Summary
特征的类型都必须定义具有此签名的方法 summarize
。
一个特质可以在其主体中拥有多个方法:方法签名按行列出,每行以分号结尾。
在类型上实现特征
现在我们已经定义了 Summary
特性方法的期望签名,我们可以在媒体聚合器中的类型上实现它。列表 10-13 显示了在 NewsArticle
结构体上实现 Summary
特性的示例,该实现使用标题、作者和位置来创建 summarize
的返回值。对于 Tweet
结构体,我们将 summarize
定义为用户名加上推文的全部内容,假设推文内容已经限制为 280 个字符。
在类型上实现特质类似于实现常规方法。不同之处在于在 impl
之后,我们放置我们想要实现的特质名称,然后使用 for
关键字,接着指定我们想要为其实现特质的类型的名称。在 impl
块内,我们放置特质定义已定义的方法签名。而不是在每个签名后添加分号,我们使用大括号并用我们希望特质的方法为特定类型具有的特定行为填充方法体。
现在库已经在 NewsArticle
和 Tweet
上实现了 Summary
特性,crate 的用户可以像调用普通方法一样在 NewsArticle
和 Tweet
的实例上调用这些特性方法。唯一的区别是用户必须将特性以及类型引入作用域。以下是一个二进制 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: 当然,正如你可能已经知道的,人们
。
其他依赖于 aggregator
crate 的 crate 也可以将 Summary
特质引入作用域,以便在它们自己的类型上实现 Summary
。需要注意的一个限制是,我们只能在特质或类型(或两者)之一是我们 crate 本地的情况下,在类型上实现特质。例如,我们可以在 aggregator
crate 功能中为自定义类型 Tweet
实现标准库特质 Display
,因为类型 Tweet
是我们 aggregator
crate 的本地类型。我们还可以在 aggregator
crate 中为 Vec<T>
实现 Summary
,因为特质 Summary
是我们 aggregator
crate 的本地特质。
但是我们不能为外部类型实现外部特征。例如,我们不能在我们的aggregator
crate 中为 Vec<T>
实现 Display
特征,因为 Display
和 Vec<T>
都定义在标准库中,并不是我们 aggregator
crate 的本地类型。这个限制是称为一致性的属性的一部分,更具体地说是孤儿规则,之所以这样命名是因为父类型不在场。这条规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有这条规则,两个 crate 可以为同一类型实现相同的特征,Rust 将不知道使用哪个实现。
默认实现
有时为特质中的某些或所有方法提供默认行为是有用的,而不是要求每个类型都实现所有方法。然后,当我们在这个特定类型上实现特质时,我们可以保留或覆盖每个方法的默认行为。
在清单 10-14 中,我们为 Summary
特性中的 summarize
方法指定了一个默认字符串,而不是像我们在清单 10-12 中那样仅定义方法签名。
要使用默认实现来总结 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());
}
这段代码打印 新文章可用!(阅读更多...)
。
创建默认实现不需要我们更改在清单 10-13 中为 Tweet
实现 Summary
的任何内容。原因是覆盖默认实现的语法与实现没有默认实现的特征方法的语法相同。
默认实现可以调用同一特质中的其他方法,即使这些其他方法没有默认实现。通过这种方式,一个特质可以提供大量有用的功能,而只需要实现者指定其中的一小部分。例如,我们可以定义 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 条新推文: (阅读更多来自 @horse_ebooks...)
。
请注意,无法从同一方法的重写实现中调用默认实现。
特质作为参数
现在你已经知道如何定义和实现特质,我们可以探讨如何使用特质来定义接受多种不同类型的函数。我们将使用在清单 10-13 中为 NewsArticle
和 Tweet
类型实现的 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
特征的任何方法,例如 summarize
。我们可以调用 notify
并传入任何 NewsArticle
或 Tweet
的实例。使用任何其他类型(如 String
或 i32
)调用该函数的代码将无法编译,因为这些类型没有实现 Summary
。
Trait Bound Syntax
impl Trait
语法适用于简单的情况,但实际上它是更长形式的语法糖,这种形式被称为 trait bound;它的样子是这样的:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
这种较长的形式与上一节中的示例等效,但更为冗长。我们在泛型类型参数的声明后使用冒号和尖括号来放置特质边界。
impl Trait
语法在简单情况下非常方便,可以使代码更加简洁,而完整的 trait bound 语法在其他情况下可以表达更多的复杂性。例如,我们可以有两个实现 Summary
的参数。使用 impl Trait
语法如下所示:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
使用 impl Trait
是合适的,如果我们希望这个函数允许 item1
和 item2
具有不同的类型(只要这两种类型都实现了 Summary
)。然而,如果我们希望强制两个参数具有相同的类型,我们必须使用一个特质边界,如下所示:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
指定为 item1
和 item2
参数类型的泛型类型 T
约束了该函数,使得传递给 item1
和 item2
的参数值的具体类型必须相同。
Specifying Multiple Trait Bounds with the +
Syntax
我们也可以指定多个特征约束。假设我们希望 notify
使用显示格式化以及对 item
使用 summarize
:我们在 notify
的定义中指定 item
必须同时实现 Display
和 Summary
。我们可以使用 +
语法来实现:
pub fn notify(item: &(impl Summary + Display)) {
+
语法在泛型类型的特征边界上也是有效的:
pub fn notify<T: Summary + Display>(item: &T) {
指定了两个 trait bound 后,notify
的主体可以调用 summarize
并使用 {}
来格式化 item
。
Clearer Trait Bounds with where
Clauses
使用过多的 trait 约束有其缺点。每个泛型都有自己的 trait 约束,因此具有多个泛型类型参数的函数在函数名称和参数列表之间可能包含大量 trait 约束信息,使得函数签名难以阅读。因此,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!()
}
这个函数的签名更加简洁:函数名、参数列表和返回类型紧密相邻,类似于没有大量特征约束的函数。
返回实现特质的类型
我们还可以在返回位置使用 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
。例如,这段代码试图返回一个 NewsArticle
或一个 Tweet
,并将其返回类型指定为 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,
}
}
}
返回一个 NewsArticle
或 Tweet
是不允许的,因为有关 impl Trait
语法在编译器中的实现存在限制。我们将在第 18 章的 “使用允许不同类型的值的特质对象” 部分介绍如何编写具有此行为的函数。
使用特质边界有条件地实现方法
通过在使用泛型类型参数的 impl
块中使用特质约束,我们可以为实现指定特质的类型有条件地实现方法。例如,类型 Pair<T>
在清单 10-15 中始终实现 new
函数以返回 Pair<T>
的新实例(回想第 5 章的 “定义方法” 部分,Self
是 impl
块类型的类型别名,在这种情况下是 Pair<T>
)。但在下一个 impl
块中,Pair<T>
仅在其内部类型 T
实现了启用比较的 PartialOrd
特质 和 启用打印的 Display
特质时,才实现 cmp_display
方法。
我们还可以为实现另一个特质的任何类型有条件地实现一个特质。在满足特质边界条件的任何类型上实现特质被称为blanket implementations,在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(); }
毯式实现出现在特征文档的“实现者”部分。
特质和特质边界让我们可以使用泛型类型参数编写代码,以减少重复,但同时指定我们希望泛型类型具有特定的行为。编译器可以使用特质边界信息来检查所有使用我们代码的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个类型未定义的方法,我们会在运行时得到错误。但是 Rust 将这些错误移到了编译时,因此我们必须在代码能够运行之前修复这些问题。此外,我们不必编写在运行时检查行为的代码,因为我们在编译时已经检查过了。这样做在不放弃泛型灵活性的情况下提高了性能。