特征:定义共享行为
特征定义了特定类型具有的功能,并且可以与其他类型共享。我们可以使用特征以抽象的方式定义共享行为。我们可以使用特征边界来指定泛型可以是具有特定行为的任何类型。
注意:特征类似于其他语言中通常称为接口的功能,尽管存在一些差异。
定义特征
类型的行为由我们可以在该类型上调用的方法组成。如果我们可以在所有这些类型上调用相同的方法,则不同类型共享相同的行为。特征定义是一种将方法签名组合在一起的方法,用于定义完成某些目的所需的一组行为。
例如,假设我们有多个结构体,它们包含各种类型和数量的文本:一个 NewsArticle
结构体,其中包含在特定位置归档的新闻报道,以及一个 Tweet
结构体,其中最多可以包含 280 个字符以及指示它是新推文、转发还是回复另一条推文的元数据。
我们想创建一个名为 aggregator
的媒体聚合器库箱,它可以显示可能存储在 NewsArticle
或 Tweet
实例中的数据的摘要。为此,我们需要每个类型提供一个摘要,我们将通过调用实例上的 summarize
方法来请求该摘要。清单 10-12 显示了表达此行为的公共 Summary
特征的定义。
文件名:src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
在这里,我们使用 trait
关键字声明一个特征,然后是特征的名称,在本例中为 Summary
。我们还将特征声明为 pub
,以便依赖于此箱的箱也可以使用此特征,我们将在几个示例中看到这一点。在大括号内,我们声明了描述实现此特征的类型的行为的方法签名,在本例中为 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)
}
}
在类型上实现特征类似于实现常规方法。区别在于,在 impl
之后,我们放置要实现的特征名称,然后使用 for
关键字,然后指定要为其实现特征的类型的名称。在 impl
块中,我们放置了特征定义已定义的方法签名。我们没有在每个签名后添加分号,而是使用大括号并用我们希望特征的方法针对特定类型具有的特定行为来填充方法体。
现在库已经在 NewsArticle
和 Tweet
上实现了 Summary
特征,箱的用户可以像调用常规方法一样在 NewsArticle
和 Tweet
的实例上调用特征方法。唯一的区别是用户必须将特征以及类型引入作用域。以下是一个二进制箱如何使用我们的 aggregator
库箱的示例
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 条新推文:horse_ebooks:当然,您可能已经知道,人们
。
依赖于 aggregator
crate 的其他 crate 也可以将 Summary
trait 引入作用域,以便在其自己的类型上实现 Summary
。需要注意的一个限制是,我们只能在类型上实现 trait,如果该 trait 或类型至少有一个是本地 crate 中的。例如,我们可以将标准库 trait(如 Display
)实现为 aggregator
crate 功能的一部分,例如 Tweet
,因为类型 Tweet
是我们 aggregator
crate 本地的。我们还可以在 aggregator
crate 中对 Vec<T>
实现 Summary
,因为 trait Summary
是我们 aggregator
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 中那样仅定义方法签名。
文件名: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)
}
}
要使用默认实现来汇总 NewsArticle
的实例,我们使用 impl Summary for NewsArticle {}
指定一个空的 impl
块。
即使我们不再直接在 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 中 Tweet
上 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 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
,我们只需要在对类型实现 trait 时定义 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
trait 为我们提供了 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...)
。
请注意,无法从同一方法的覆盖实现中调用默认实现。
作为参数的 Trait
现在您已经知道了如何定义和实现 trait,我们可以探索如何使用 trait 来定义接受多种不同类型的函数。我们将使用在代码清单 10-13 中对 NewsArticle
和 Tweet
类型实现的 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 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
关键字和 trait 名称,而不是 item
参数的具体类型。此参数接受实现指定 trait 的任何类型。在 notify
的主体中,我们可以调用 item
上来自 Summary
trait 的任何方法,例如 summarize
。我们可以调用 notify
并传入 NewsArticle
或 Tweet
的任何实例。使用任何其他类型(例如 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) {
指定为 item1
和 item2
参数类型的泛型类型 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 绑定,因此具有多个泛型类型参数的函数可以在函数名称及其参数列表之间包含大量 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 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 Summary
的返回类型返回 NewsArticle
或 Tweet
将不起作用
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
语法的实现方式存在限制,因此不允许返回 NewsArticle
或 Tweet
。我们将在第 17 章的“使用允许不同类型值的 Trait 对象”一节中介绍如何编写具有此行为的函数。
使用特征边界有条件地实现方法
通过在使用泛型类型参数的 impl
块中使用特征边界,我们可以有条件地为实现指定特征的类型实现方法。例如,清单 10-15 中的类型 Pair<T>
始终实现 new
函数以返回 Pair<T>
的新实例(回想一下第 5 章““定义方法”部分,Self
是 impl
块类型的类型别名,在本例中为 Pair<T>
)。但在下一个 impl
块中,Pair<T>
仅在其内部类型 T
实现启用比较的 PartialOrd
特征以及启用打印的 Display
特征时才实现 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);
}
}
}
我们还可以为实现另一个特征的任何类型有条件地实现一个特征。对满足特征边界的任何类型的特征实现称为覆盖实现,并在 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 将这些错误转移到编译时,因此我们不得不在代码能够运行之前修复问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时进行了检查。这样做在不放弃泛型的灵活性的情况下提高了性能。