开发环境
- Windows 10
- Rust 1.70.0
- VS Code 1.79.2
项目工程
这里继续沿用上次工程rust-demo
Traits:定义共同的行为
Trait定义了一个特定类型所具有的功能,并且可以与其他类型共享。我们可以使用特质以抽象的方式来定义共享行为。我们可以使用特质的界限来指定一个通用类型可以是任何具有某些行为的类型。
注意:traits类似于其他语言中通常称为接口的特性,尽管有一些不同之处。
定义Trait
一个类型的行为包括我们可以在该类型上调用的方法。如果我们可以在所有这些类型上调用相同的方法,那么不同的类型就有相同的行为。特质定义是一种将方法签名组合在一起的方式,以定义一组完成某些目的所需的行为。
例如,假设我们有多个结构来保存各种类型和数量的文本:一个NewsArticle结构,保存在特定地点的新闻报道;一个Tweet结构,最多可以有280个字符,还有元数据,表明它是一个新的Tweet,一个转发,或对另一个Tweet的回复。
我们想做一个名为aggregator的媒体聚合库,可以显示可能存储在NewsArticle或Tweet实例中的数据摘要。要做到这一点,我们需要每种类型的摘要,我们将通过调用实例上的summaryize方法来请求该摘要。示例12显示了一个表达这种行为的公共Summary属性的定义。
文件名:src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
示例12:一个由summaryize方法所提供的行为组成的Summary Trait
在这里,我们使用 trait 关键字声明一个 trait,然后是 trait 的名字,在这个例子中是 Summary。我们还将该特性声明为pub,这样依赖于该板条箱的板条箱也可以使用该特性,我们将在几个例子中看到。在大括号内,我们声明了描述实现该特性的类型的行为的方法签名,在本例中是 fn summarize(&self) -> String。
在方法签名之后,我们没有在大括号内提供一个实现,而是使用了一个分号。每个实现trait的类型都必须为该方法的主体提供自己的自定义行为。编译器会强制要求任何具有Summary trait的类型都要用这个签名定义summaryize方法。
一个trait在其主体中可以有多个方法:方法签名每行列出一个,每行以分号结尾。
在一个类型上实现一个Trait
现在我们已经定义了Summary trait方法的所需签名,我们可以在我们的媒体聚合器中的类型上实现它。示例13 显示了在 NewsArticle 结构上对 Summary 特质的实现,它使用标题、作者和位置来创建 summaryize 的返回值。对于Tweet结构,我们将summaryize定义为用户名和整个tweet的文本,假设tweet内容已经被限制在280个字符以内。
文件名:src/lib.rs
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)
}
}
示例13:在NewsArticle和Tweet类型上实现Summary Trait
在一个类型上实现特质与实现常规方法类似。不同的是,在 impl 之后,我们要写上我们想要实现的特质名称,然后使用 for 关键字,再指定我们想要实现特质的类型名称。在 impl 块中,我们把特质定义中的方法签名放进去。我们没有在每个签名后面添加分号,而是使用大括号,并在方法体中填写我们希望特质的方法对特定类型的具体行为。
现Tweet在库已经在NewsArticle和Tweet上实现了Summary特性,crate的用户可以在NewsArticle和的实例上调用特性方法,就像我们调用常规方法一样。唯一的区别是,用户必须把特质和类型都带入范围。下面是一个二进制板条箱如何使用我们的聚合器库板条箱的例子:
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板块的其他板块也可以将 Summary 特质带入范围,在自己的类型上实现 Summary。需要注意的一个限制是,只有当特质或类型中至少有一个是我们板条箱的本地类型时,我们才能在一个类型上实现特质。例如,我们可以在自定义类型(如Tweet)上实现标准库特性(如Display),作为我们聚合器板块功能的一部分,因为Tweet这个类型是我们聚合器板块的本地。我们也可以在我们的聚合器箱中实现Vec<T>的Summary,因为Summary这个特性是我们aggregator箱的本地特性。
但是我们不能在外部类型上实现外部特性。例如,我们不能在我们的aggregator箱中对 Vec<T> 实现 Display 特质,因为 Display 和 Vec<T> 都是在标准库中定义的,并不是我们aggregator箱的本地类型。这个限制是一个叫做一致性的属性的一部分,更确切地说,是孤儿规则,因为父类型不存在而被命名。这个规则确保其他人的代码不会破坏你的代码,反之亦然。如果没有这个规则,两个板块可以为同一类型实现相同的特性,而Rust不知道该使用哪个实现。
默认实现
有时,为特质中的某些或所有方法设置默认行为,而不是要求对每个类型的所有方法进行实现,是非常有用的。然后,当我们在一个特定类型上实现特质时,我们可以保留或覆盖每个方法的默认行为。
在示例14中,我们为Summary trait的summaryize方法指定了一个默认字符串,而不是像示例12中那样只定义方法签名。
文件名:src/lib.rs
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
示例14: 定义一个带有summaryize方法的默认实现的Summary特质
为了使用默认的实现来总结NewsArticle的实例,指定一个空的代码块:impl Summary for NewsArticle {}。
尽管我们不再直接定义NewsArticle的summaryize方法,但我们已经提供了一个默认的实现,并指定NewsArticle实现了Summary特性。因此,我们仍然可以在NewsArticle的一个实例上调用summaryize方法,就像这样:
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...)
创建一个缺省实现并不要求我们改变示例13中Summary on Tweet的实现。原因是覆盖默认实现的语法与实现没有默认实现的特质方法的语法相同。
默认实现可以调用同一特质中的其他方法,即使这些其他方法没有默认实现。通过这种方式,一个trait可以提供很多有用的功能,而只需要实现者指定其中的一小部分。例如,我们可以定义Summary trait,使其有一个需要实现的summaryize_author方法,然后定义一个summaryize方法,该方法有一个调用summaryize_author方法的默认实现:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
要使用这个版本的Summary,我们只需要在类型上实现该特性时定义summaryize_author:
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
在我们定义了summaryize_author之后,我们可以在Tweet结构的实例上调用summaryize,而summaryize的默认实现将调用我们提供的summaryize_author的定义。因为我们已经实现了summaryize_author,Summary trait已经给了我们summaryize方法的行为,而不需要我们再写任何代码。
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...)
注意,不可能从同一方法的重写实现中调用默认实现。
作为参数的Traits
现在你知道了如何定义和实现Trait,我们可以探索如何使用特质来定义接受许多不同类型的函数。我们将在示例13中使用我们在NewsArticle和Tweet类型上实现的Summary trait来定义一个通知函数,该函数在其item参数上调用summaryize方法,该参数是一些实现了Summary trait的类型。要做到这一点,我们使用 impl Trait 语法,像这样:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
我们为item参数指定 impl 关键字和trait名称,而不是具体的类型。这个参数接受任何实现了指定trait的类型。在 notify 的主体中,我们可以调用 item上来自 Summary 特质的任何方法,比如 summaryize。我们可以调用notify 并传入NewsArticle或Tweet的任何实例。用任何其他类型(如String或i32)调用该函数的代码将不会被编译,因为这些类型没有实现Summary 。
Trait Bound句法
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) {
如果我们想让这个函数允许 item1 和 item2 有不同的类型(只要两个类型都实现了 Summary),那么使用 impl Trait是合适的。然而,如果我们想强制两个参数具有相同的类型,我们必须使用trait bound,像这样:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
被指定为item1和item2参数类型的通用类型T约束了该函数,使得作为item1和item2参数传递的值的具体类型必须是相同的。
用 "+"语法指定多个Traits Bound
我们还可以指定一个以上的trait bound。假设我们想让notify在item上使用显示格式以及summary:我们在notify的定义中指定item必须同时实现Display和summary。我们可以使用 "+"语法来这样做:
pub fn notify(item: &(impl Summary + Display)) {
+ 语法在通用类型的trait bound上也是有效的:
pub fn notify<T: Summary + Display>(item: &T) {
有了这两个trait bound的指定,notify的主体可以调用summaryize并使用{}来格式化项目。
带有where子句的更清晰的Traits Bound
使用过多的trait bound也有其弊端。每个泛型都有自己的trait bound,所以有多个泛型参数的函数在函数名和参数列表之间会包含很多trait bound信息,使得函数签名难以阅读。出于这个原因,Rust在函数签名后的where子句中提供了另一种语法来指定trait bound。所以不要写成这样:
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,
{
这个函数的签名不那么杂乱:函数名、参数列表和返回类型紧挨着,类似于没有大量特质界限的函数。
返回实现Traits的类型
我们也可以在返回位置使用 impl Trait 语法来返回某个实现了特质的类型的值,如下所示:
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,但调用此函数的代码不需要知道这一点。
在闭包和迭代器的上下文中,只通过它所实现的特征来指定返回类型的能力特别有用,我们在后续章中介绍了这一点。闭包和迭代器创建了只有编译器知道的类型,或者指定了非常长的类型。impl Trait语法可以让你简洁地指定一个函数返回某个实现了Iterator trait的类型,而不需要写出一个很长的类型。
然而,你只能在返回单一类型的时候使用 impl Trait。例如,这段返回NewsArticle或Tweet的代码,其返回类型被指定为implum Summary,则无法使用:
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是不允许的。
使用Traits Bound有条件地实现方法
通过使用trait与使用通用类型参数的impl块绑定,我们可以为实现指定trait的类型有条件地实现方法。例如,示例15 中的类型 Pair<T> 总是实现 new 函数,以返回Pair<T> 的新实例(记得在前面章的 "定义方法 "一节中,Self 是植入块的类型的别名,在这里是 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);
}
}
}
示例15: 根据trait bound,有条件地在通用类型上实现方法
我们也可以对任何实现了另一个trait 的类型有条件地实现一个trait。在任何满足trait bound的类型上实现trait 被称为空白实现,在Rust标准库中被广泛使用。例如,标准库在任何实现了Display特质的类型上实现了ToString特质。标准库中的植入块看起来类似于这段代码:
impl<T: Display> ToString for T {
// --snip--
}
因为标准库有这种很多的实现,我们可以在任何实现了Display特质的类型上调用ToString特质所定义的to_string方法。例如,我们可以像这样把整数变成它们相应的字符串值,因为整数实现了Display:
let s = 3.to_string();
空白的实现出现在trait的文档中的 "实施者 "部分。
trait和trait bound让我们在写代码时可以使用通用类型的参数来减少重复,同时也可以向编译器说明我们希望通用类型具有特定的行为。然后,编译器可以使用特质约束信息来检查我们代码中使用的所有具体类型是否提供了正确的行为。在动态类型语言中,如果我们在一个没有定义方法的类型上调用一个方法,我们会在运行时得到一个错误。但是Rust将这些错误转移到了编译时,所以我们不得不在代码运行之前就修复这些问题。此外,我们不必在运行时编写检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。
本章重点
- Trait概念
- 定义Trait
- 基于具体类型实现Trait
- Trait的默认实现
- Trait作为参数的传递方法
- 实现了Trait的返回类型