使用 Trait 定义共享行为
trait 定义了某种类型所具有、并可与其他类型共享的功能。我们可以用 trait 以抽象方式定义共享行为,也可以使用 trait 约束指定泛型类型可以是任何具有特定行为的类型。
注意:trait 与其他语言中常称为接口的功能类似,但两者也有一些区别。
定义 Trait
类型的行为由我们可以在该类型上调用的方法构成。如果可以在不同类型上调用相同的方法,这些类型就具有相同的行为。Trait 定义可以将方法签名组合在一起,从而定义实现某种目的所需的一组行为。
例如,假设我们有多个结构体,用来保存不同种类和数量的文本:NewsArticle 结构体保存一篇在特定地点发布的新闻;SocialPost 最多可包含 280 个字符,并带有元数据,用来说明它是新帖子、转发帖子,还是对另一条帖子的回复。
我们想要创建一个名为 aggregator 的媒体聚合器库 crate,用来展示可能保存在 NewsArticle 或 SocialPost 实例中的数据摘要。为此,我们需要从每种类型中获取摘要,并通过调用实例上的 summarize 方法来请求摘要。清单 10-12 展示了一个公有 Summary trait 的定义,它表达了这种行为。
pub trait Summary {
fn summarize(&self) -> String;
}
summarize 方法提供行为的 Summary trait这里,我们使用 trait 关键字声明一个 trait,后面跟着 trait 的名称,本例中为 Summary。我们还将 trait 声明为 pub,这样依赖此 crate 的其他 crate 也能使用它,后面会看到一些例子。在花括号中,我们声明方法签名,用来描述实现该 trait 的类型所具有的行为;本例中的签名是 fn summarize(&self) -> String。
方法签名后面没有在花括号中提供实现,而是使用分号。每种实现此 trait 的类型都必须为方法体提供自己的行为。编译器会强制要求所有具有 Summary trait 的类型都必须严格按照此签名定义 summarize 方法。
Trait 中可以包含多个方法:每个方法签名各占一行,并以分号结尾。
为类型实现 Trait
现在我们已经定义了 Summary trait 方法所需的签名,可以为媒体聚合器中的类型实现该 trait。清单 10-13 展示了为 NewsArticle 结构体实现 Summary trait 的方式:使用标题、作者和地点生成 summarize 的返回值。对于 SocialPost 结构体,我们将 summarize 定义为用户名加上帖子的完整文本,并假设帖子内容已经限制在 280 个字符以内。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
NewsArticle 和 SocialPost 类型实现 Summary trait为类型实现 trait 与实现普通方法类似。区别在于,impl 后面先写要实现的 trait 名称,再使用 for 关键字,然后指定要为其实现 trait 的类型名称。在 impl 块中,我们填写 trait 定义中声明的方法签名。与在每个签名后添加分号不同,这里使用花括号,并在方法体中填写该 trait 的方法对特定类型应有的具体行为。
现在,这个库已经为 NewsArticle 和 SocialPost 实现了 Summary trait,crate 的使用者就可以像调用普通方法一样,在这两种类型的实例上调用 trait 方法。唯一的区别是,使用者必须将 trait 和类型一同引入作用域。下面是二进制 crate 使用我们的 aggregator 库 crate 的示例:
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
这段代码会打印 1 new post: horse_ebooks: of course, as you probably already know, people。
依赖 aggregator crate 的其他 crate 也可以将 Summary trait 引入作用域,并为自己的类型实现 Summary。需要注意的一项限制是:只有当 trait 或类型(或两者)属于当前 crate 时,我们才能为该类型实现这个 trait。例如,由于 SocialPost 类型属于我们的 aggregator crate,因此可以在其中为自定义类型 SocialPost 实现 Display 等标准库 trait。我们也可以在 aggregator crate 中为 Vec<T> 实现 Summary,因为 Summary trait 属于该 crate。
但我们不能为外部类型实现外部 trait。例如,不能在 aggregator crate 中为 Vec<T> 实现 Display trait,因为 Display 和 Vec<T> 都定义在标准库中,并不属于 aggregator crate。此限制属于称为一致性的性质,更具体地说是孤儿规则;之所以这样称呼,是因为父类型并不存在。该规则确保其他人的代码不会破坏你的代码,反之亦然。没有这条规则,两个 crate 可能为同一类型实现同一个 trait,Rust 就无法确定应该使用哪个实现。
使用默认实现
有时,为 trait 中部分或全部方法提供默认行为很有用,这样就不必要求每种类型都实现所有方法。为特定类型实现 trait 时,我们可以保留或覆盖每个方法的默认行为。
清单 10-14 为 Summary trait 的 summarize 方法指定了默认字符串,而不是像清单 10-12 那样只定义方法签名。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
summarize 方法默认实现的 Summary trait要使用默认实现来总结 NewsArticle 实例,我们只需提供一个空的 impl 块:impl Summary for NewsArticle {}。
虽然我们不再直接为 NewsArticle 定义 summarize 方法,但我们已经提供了默认实现,并指定 NewsArticle 实现了 Summary trait。因此,仍然可以像下面这样在 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 中 SocialPost 对 Summary 的实现。这是因为覆盖默认实现的语法,与实现一个没有默认实现的 trait 方法的语法相同。
默认实现可以调用同一 trait 中的其他方法,即使那些方法没有默认实现。这样,trait 就能提供许多有用功能,同时只要求实现者指定其中一小部分。例如,我们可以在 Summary trait 中定义一个必须实现的 summarize_author 方法,然后定义一个默认实现为调用 summarize_author 的 summarize 方法:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
要使用这个版本的 Summary,只需在为某个类型实现 trait 时定义 summarize_author:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
定义 summarize_author 后,我们就可以在 SocialPost 结构体实例上调用 summarize;summarize 的默认实现会调用我们提供的 summarize_author 定义。由于我们已经实现了 summarize_author,Summary trait 就为我们提供了 summarize 方法的行为,无需再编写其他代码。示例如下:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
这段代码会打印 1 new post: (Read more from @horse_ebooks...)。
请注意,不能在某个方法自己的覆盖实现中调用该方法的默认实现。
将 Trait 用作参数
现在你已经了解如何定义和实现 trait,接下来看看如何使用 trait 定义能够接受多种不同类型的函数。我们会使用清单 10-13 中为 NewsArticle 和 SocialPost 实现的 Summary trait,定义一个 notify 函数。它会在 item 参数上调用 summarize 方法,而该参数属于某种实现了 Summary 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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
我们没有为 item 参数指定具体类型,而是指定 impl 关键字和 trait 名称。该参数可以接受任何实现了指定 trait 的类型。在 notify 函数体中,我们可以调用 item 上由 Summary trait 提供的任何方法,例如 summarize。调用 notify 时,可以传入任意 NewsArticle 或 SocialPost 实例。若传入其他类型(例如 String 或 i32),代码将无法编译,因为这些类型没有实现 Summary。
Trait 约束语法
impl Trait 语法适用于简单情况,但它实际上是更完整形式的语法糖,这种形式称为 trait 约束,如下所示:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
这种较完整的形式与上一节的示例等价,但写法更冗长。我们在冒号后、尖括号内的泛型类型参数声明中放置 trait 约束。
impl Trait 语法很方便,在简单情况下能让代码更简洁;而更完整的 trait 约束语法则能表达更复杂的情形。例如,我们可以让两个参数都实现 Summary。使用 impl Trait 语法时如下所示:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
如果希望函数允许 item1 和 item2 使用不同类型(只要两种类型都实现了 Summary),使用 impl Trait 就很合适。不过,如果要强制两个参数具有相同类型,就必须使用 trait 约束,如下所示:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
将泛型类型 T 指定为 item1 和 item2 参数的类型,会对函数形成约束:传入这两个参数的值必须具有相同的具体类型。
使用 + 语法指定多个 Trait 约束
我们也可以指定多个 trait 约束。假设希望 notify 除了对 item 调用 summarize,还能使用显示格式化:那么就要在 notify 定义中指定 item 必须同时实现 Display 和 Summary。可以使用 + 语法实现:
pub fn notify(item: &(impl Summary + Display)) {
+ 语法也可以用于泛型类型的 trait 约束:
pub fn notify<T: Summary + Display>(item: &T) {
指定这两个 trait 约束后,notify 的函数体就可以调用 summarize,并使用 {} 格式化 item。
使用 where 子句让 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!()
}
这个函数签名不那么杂乱:函数名、参数列表和返回类型彼此相邻,类似于没有大量 trait 约束的函数。
返回实现 Trait 的类型
我们也可以在返回位置使用 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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
将返回类型指定为 impl Summary,表示 returns_summarizable 函数会返回某种实现了 Summary trait 的类型,但不必指出具体类型。本例中,returns_summarizable 返回 SocialPost,但调用此函数的代码无需知道这一点。
只通过类型实现的 trait 来指定返回类型的能力,在闭包和迭代器的场景中特别有用,第 13 章会介绍它们。闭包和迭代器会创建只有编译器知道的类型,或者很难完整写出的长类型。impl Trait 语法让你无需写出很长的类型,就能简洁地指定函数返回某种实现了 Iterator trait 的类型。
不过,只有在返回单一类型时才能使用 impl Trait。例如,下面这段代码会返回 NewsArticle 或 SocialPost,并将返回类型指定为 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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
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 {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
由于编译器实现 impl Trait 语法的方式受到限制,因此不允许返回 NewsArticle 或 SocialPost。第 18 章的“使用 Trait 对象抽象共享行为”一节会介绍如何编写具有此行为的函数。
使用 Trait 约束有条件地实现方法
在使用泛型类型参数的 impl 块中加入 trait 约束,就能有条件地为实现了指定 trait 的类型实现方法。例如,清单 10-15 中的 Pair<T> 类型始终会实现 new 函数,以返回新的 Pair<T> 实例(回想第 5 章“方法语法”一节,Self 是 impl 块所对应类型的别名,本例中即为 Pair<T>)。但在下一个 impl 块中,只有当 Pair<T> 的内部类型 T 实现了支持比较的 PartialOrd trait 以及支持打印的 Display trait 时,才会实现 cmp_display 方法。
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);
}
}
}
我们还可以为任何实现了另一个 trait 的类型有条件地实现某个 trait。为所有满足 trait 约束的类型实现 trait,称为 blanket 实现,Rust 标准库广泛使用了这种方式。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。标准库中的 impl 块类似于下面的代码:
impl<T: Display> ToString for T {
// --snip--
}
由于标准库提供了这种 blanket 实现,我们可以在任何实现了 Display trait 的类型上调用 ToString trait 定义的 to_string 方法。例如,整数实现了 Display,因此可以像下面这样将整数转换为对应的 String 值:
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
Trait 文档中的“实现者”部分会列出 blanket 实现。
Trait 和 trait 约束让我们能够编写使用泛型类型参数来减少重复的代码,同时也能告知编译器我们要求泛型类型具有特定行为。随后,编译器可以利用 trait 约束信息,检查代码所使用的所有具体类型是否提供了正确行为。在动态类型语言中,如果对没有定义某方法的类型调用该方法,就会在运行时出错。而 Rust 会将这类错误提前到编译时,迫使我们在代码运行前修复问题。此外,由于已在编译时完成检查,我们无需编写在运行时检查行为的代码。这样既能提高性能,又不必放弃泛型的灵活性。