特性:定义共享行为
特性定义了特定类型所具有的功能,并且可以与其他类型共享。我们可以使用特性以抽象的方式定义共享行为。我们可以使用特性约束来指定泛型类型可以是任何具有特定行为的类型。
注意:特性类似于其他语言中通常称为接口的功能,尽管存在一些差异。
定义特性
类型的行为由我们可以在该类型上调用的方法组成。如果我们可以对所有这些类型调用相同的方法,则不同的类型共享相同的行为。特性定义是一种将方法签名组合在一起以定义完成某些目的所需的一组行为的方式。
例如,假设我们有多个结构体,用于保存各种类型和数量的文本:一个 NewsArticle
结构体,用于保存特定位置的新闻报道;一个 Tweet
结构体,最多可以有 280 个字符以及指示它是新推文、转发推文还是回复另一条推文的元数据。
我们想要创建一个名为 aggregator
的媒体聚合库 crate,它可以显示可能存储在 NewsArticle
或 Tweet
实例中的数据的摘要。为此,我们需要从每种类型中获取摘要,并且我们将通过在实例上调用 summarize
方法来请求该摘要。列表 10-12 显示了公共 Summary
特性的定义,该特性表达了这种行为。
pub trait Summary {
fn summarize(&self) -> String;
}
Summary
特性,由 summarize
方法提供的行为组成在这里,我们使用 trait
关键字声明一个特性,然后是特性的名称,在本例中为 Summary
。我们还将特性声明为 pub
,以便依赖于此 crate 的 crate 也可以使用此特性,正如我们将在几个示例中看到的那样。在花括号内,我们声明了描述实现此特性的类型的行为的方法签名,在本例中为 fn summarize(&self) -> String
。
在方法签名之后,我们使用分号而不是在花括号内提供实现。每个实现此特性的类型都必须为该方法的主体提供自己的自定义行为。编译器将强制任何具有 Summary
特性的类型都必须精确地定义具有此签名的方法 summarize
。
一个特性可以在其主体中包含多个方法:方法签名每行列出一个,并且每行以分号结尾。
在类型上实现特性
现在我们已经定义了 Summary
特性的方法的所需签名,我们可以在我们的媒体聚合器中的类型上实现它。列表 10-13 显示了在 NewsArticle
结构体上实现 Summary
特性的示例,它使用标题、作者和位置来创建 summarize
的返回值。对于 Tweet
结构体,我们将 summarize
定义为用户名后跟推文的全部文本,假设推文内容已经限制为 280 个字符。
NewsArticle
和 Tweet
类型上实现 Summary
特性在类型上实现特性类似于实现常规方法。不同之处在于,在 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: 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
特性,因为 Display
和 Vec<T>
都在标准库中定义,并且不是本地于我们的 aggregator
crate 的。此限制是称为一致性的属性的一部分,更具体地说是孤儿规则,之所以这样命名是因为父类型不存在。此规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有此规则,则两个 crate 可以为同一类型实现相同的特性,而 Rust 将不知道要使用哪个实现。
默认实现
有时,对于特性中的某些或所有方法,拥有默认行为而不是要求每个类型上的所有方法都进行实现会很有用。然后,当我们在一个特定类型上实现特性时,我们可以保留或覆盖每个方法的默认行为。
在列表 10-14 中,我们为 Summary
特性的 summarize
方法指定了一个默认字符串,而不是像我们在列表 10-12 中所做的那样仅定义方法签名。
summarize
方法的默认实现的 Summary
特性要使用默认实现来总结 NewsArticle
的实例,我们指定一个空的 impl
代码块,其中包含 impl Summary for NewsArticle {}
。
即使我们不再直接在 NewsArticle
上定义 summarize
方法,我们也提供了默认实现,并指定 NewsArticle
实现了 Summary
特性。因此,我们仍然可以在 NewsArticle
的实例上调用 summarize
方法,如下所示
此代码打印 New article available! (Read more...)
。
创建默认实现不需要我们更改列表 10-13 中 Summary
在 Tweet
上的实现的任何内容。原因是覆盖默认实现的语法与实现没有默认实现的特性方法的语法相同。
默认实现可以调用同一特性中的其他方法,即使这些其他方法没有默认实现也是如此。通过这种方式,特性可以提供许多有用的功能,并且仅需要实现者指定其中的一小部分。例如,我们可以将 Summary
特性定义为具有一个 summarize_author
方法(其实现是必需的),然后定义一个 summarize
方法,该方法具有调用 summarize_author
方法的默认实现
要使用此版本的 Summary
,我们只需在类型上实现特性时定义 summarize_author
即可
在我们定义 summarize_author
之后,我们可以在 Tweet
结构体的实例上调用 summarize
,并且 summarize
的默认实现将调用我们提供的 summarize_author
的定义。因为我们已经实现了 summarize_author
,所以 Summary
特性为我们提供了 summarize
方法的行为,而无需我们编写任何更多代码。这就是它的外观
此代码打印 1 new tweet: (Read more from @horse_ebooks...)
。
请注意,无法从同一方法的覆盖实现中调用默认实现。
特性作为参数
现在您已经了解了如何定义和实现特性,我们可以探索如何使用特性来定义接受许多不同类型的函数。我们将使用我们在列表 10-13 中的 NewsArticle
和 Tweet
类型上实现的 Summary
特性来定义一个 notify
函数,该函数在其 item
参数上调用 summarize
方法,该参数是实现 Summary
特性的某种类型。为此,我们使用 impl Trait
语法,如下所示
对于 item
参数,我们没有指定具体类型,而是指定了 impl
关键字和特性名称。此参数接受任何实现指定特性的类型。在 notify
的主体中,我们可以调用来自 Summary
特性的 item
上的任何方法,例如 summarize
。我们可以调用 notify
并传入 NewsArticle
或 Tweet
的任何实例。使用任何其他类型(例如 String
或 i32
)调用该函数的代码将无法编译,因为这些类型未实现 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) {
如果我们希望此函数允许 item1
和 item2
具有不同的类型(只要这两种类型都实现 Summary
),则使用 impl Trait
是合适的。但是,如果我们想强制两个参数具有相同的类型,则必须使用特性约束,如下所示
pub fn notify<T: Summary>(item1: &T, item2: &T) {
指定为 item1
和 item2
参数类型的泛型类型 T
约束该函数,使得作为 item1
和 item2
参数的实参传递的值的具体类型必须相同。
使用 +
语法指定多个特性约束
我们还可以指定多个特性约束。假设我们希望 notify
在 item
上使用显示格式以及 summarize
:我们在 notify
定义中指定 item
必须同时实现 Display
和 Summary
。我们可以使用 +
语法来实现
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
子句,如下所示
此函数的签名不太混乱:函数名称、参数列表和返回类型紧密地结合在一起,类似于没有大量特性约束的函数。
返回实现特性的类型
我们还可以在返回位置使用 impl Trait
语法来返回实现特性的某种类型的值,如下所示
通过对返回类型使用 impl Summary
,我们指定 returns_summarizable
函数返回某种实现 Summary
特性的类型,而无需命名具体类型。在本例中,returns_summarizable
返回 Tweet
,但是调用此函数的代码不需要知道这一点。
仅通过它实现的特性来指定返回类型的功能在闭包和迭代器的上下文中特别有用,我们将在第 13 章中介绍它们。闭包和迭代器创建只有编译器知道的类型或非常长才能指定的类型。impl Trait
语法使您可以简洁地指定函数返回某种实现 Iterator
特性的类型,而无需写出很长的类型。
但是,如果仅返回单个类型,则只能使用 impl Trait
。例如,此代码返回 NewsArticle
或 Tweet
之一,并将返回类型指定为 impl Summary
将不起作用
由于围绕编译器中 impl Trait
语法的实现方式的限制,不允许返回 NewsArticle
或 Tweet
之一。我们将在第 18 章的 “使用允许不同类型值的特性对象”部分介绍如何编写具有此行为的函数。
使用特性约束有条件地实现方法
通过将特性约束与使用泛型类型参数的 impl
代码块一起使用,我们可以为实现指定特性的类型有条件地实现方法。例如,列表 10-15 中的类型 Pair<T>
始终实现 new
函数以返回 Pair<T>
的新实例(回想一下第 5 章的 “定义方法”部分,Self
是 impl
代码块类型的类型别名,在本例中为 Pair<T>
)。但是在下一个 impl
代码块中,只有当其内部类型 T
实现启用比较的 PartialOrd
特性和启用打印的 Display
特性时,Pair<T>
才实现 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);
}
}
}
我们还可以有条件地为任何实现另一个特性的类型实现特性。在任何满足特性约束的类型上实现特性都称为覆盖实现,并在 Rust 标准库中广泛使用。例如,标准库在任何实现 Display
特性的类型上实现 ToString
特性。标准库中的 impl
代码块类似于以下代码
impl<T: Display> ToString for T {
// --snip--
}
由于标准库具有此覆盖实现,因此我们可以对任何实现 Display
特性的类型调用 ToString
特性定义的 to_string
方法。例如,我们可以像这样将整数转换为其对应的 String
值,因为整数实现了 Display
覆盖实现出现在特性的文档的“Implementors”部分中。
特性和特性约束使我们能够编写使用泛型类型参数来减少重复的代码,但也向编译器指定我们希望泛型类型具有特定的行为。然后,编译器可以使用特性约束信息来检查与我们的代码一起使用的所有具体类型是否提供正确的行为。在动态类型语言中,如果我们在未定义方法的类型上调用方法,则会在运行时收到错误。但是 Rust 将这些错误移至编译时,因此我们被迫在代码甚至能够运行之前修复这些问题。此外,我们不必编写代码来检查运行时的行为,因为我们已经在编译时进行了检查。这样做可以提高性能,而无需放弃泛型的灵活性。