17 KiB
trait:定义共享的行为
ch10-02-traits.md
commit e5a987f5da3fba24e55f5c7102ec63f9dc3bc360
trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。
注意:trait 类似于其他语言中的常被称为接口(interfaces)的功能,虽然有一些不同。
定义 trait
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体NewsArticle
用于存放发生于世界各地的新闻故事,而结构体Tweet
最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个多媒体聚合库用来显示可能储存在NewsArticle
或Tweet
实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summary
方法来请求总结。列表 10-11 中展示了一个表现这个概念的Summarizable
trait 的定义:
Filename: lib.rs
pub trait Summarizable {
fn summary(&self) -> String;
}
Listing 10-11: Definition of a Summarizable
trait that
consists of the behavior provided by a summary
method
使用trait
关键字来定义一个 trait,后面是 trait 的名字,在这个例子中是Summarizable
。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是fn summary(&self) -> String
。在方法签名后跟分号而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现Summarizable
trait 的类型都拥有与这个签名的定义完全一致的summary
方法。
trait 体中可以有多个方法,一行一个方法签名且都以分号结尾。
为类型实现 trait
现在我们定义了Summarizable
trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。列表 10-12 中展示了NewsArticle
结构体上Summarizable
trait 的一个实现,它使用标题、作者和创建的位置作为summary
的返回值。对于Tweet
结构体,我们选择将summary
定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。
Filename: lib.rs
# pub trait Summarizable {
# fn summary(&self) -> String;
# }
#
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summarizable for NewsArticle {
fn summary(&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 Summarizable for Tweet {
fn summary(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Listing 10-12: Implementing the Summarizable
trait on
the NewsArticle
and Tweet
types
在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl
关键字之后,我们提供需要实现 trait 的名称,接着是for
和需要实现 trait 的类型的名称。在impl
块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
一旦实现了 trait,我们就可以用与NewsArticle
和Tweet
实例的非 trait 方法一样的方式调用 trait 方法了:
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.summary());
这会打印出1 new tweet: horse_ebooks: of course, as you probably already know, people
。
注意因为列表 10-12 中我们在相同的lib.rs
里定义了Summarizable
trait 和NewsArticle
与Tweet
类型,所以他们是位于同一作用域的。如果这个lib.rs
是对应aggregator
crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast
结构体实现Summarizable
trait,在实现Summarizable
trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:
Filename: lib.rs
extern crate aggregator;
use aggregator::Summarizable;
struct WeatherForecast {
high_temp: f64,
low_temp: f64,
chance_of_precipitation: f64,
}
impl Summarizable for WeatherForecast {
fn summary(&self) -> String {
format!("The high will be {}, and the low will be {}. The chance of
precipitation is {}%.", self.high_temp, self.low_temp,
self.chance_of_precipitation)
}
}
Listing 10-13: Bringing the Summarizable
trait from our
aggregator
crate into scope in another crate
另外这段代码假设Summarizable
是一个公有 trait,这是因为列表 10-11 中trait
之前使用了pub
关键字。
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec
上实现Display
trait,因为Display
和Vec
都定义于标准库中。允许在像Tweet
这样作为我们aggregator
crate 部分功能的自定义类型上实现标准库中的 trait Display
。也允许在aggregator
crate中为Vec
实现Summarizable
,因为Summarizable
定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
默认实现
有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
列表 10-14 中展示了如何为Summarize
trait 的summary
方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:
Filename: lib.rs
pub trait Summarizable {
fn summary(&self) -> String {
String::from("(Read more...)")
}
}
Listing 10-14: Definition of a Summarizable
trait with
a default implementation of the summary
method
如果想要对NewsArticle
实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl
块:
impl Summarizable for NewsArticle {}
即便选择不再直接为NewsArticle
定义summary
方法了,因为summary
方法有一个默认实现而且NewsArticle
被指定为实现了Summarizable
trait,我们仍然可以对NewsArticle
的实例调用summary
方法:
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.summary());
这段代码会打印New article available! (Read more...)
。
将Summarizable
trait 改变为拥有默认summary
实现并不要求对列表 10-12 中的Tweet
和列表 10-13 中的WeatherForecast
对Summarizable
的实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让Summarizable
trait 也拥有一个要求实现的author_summary
方法,接着summary
方法则提供默认实现并调用author_summary
方法:
pub trait Summarizable {
fn author_summary(&self) -> String;
fn summary(&self) -> String {
format!("(Read more from {}...)", self.author_summary())
}
}
为了使用这个版本的Summarizable
,只需在实现 trait 时定义author_summary
即可:
impl Summarizable for Tweet {
fn author_summary(&self) -> String {
format!("@{}", self.username)
}
}
一旦定义了author_summary
,我们就可以对Tweet
结构体的实例调用summary
了,而summary
的默认实现会调用我们提供的author_summary
定义。
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.summary());
这会打印出1 new tweet: (Read more from @horse_ebooks...)
。
注意在重载过的实现中调用默认实现是不可能的。
trait bounds
现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 trait bounds。
例如在列表 10-12 中为NewsArticle
和Tweet
类型实现了Summarizable
trait。我们可以定义一个函数notify
来调用summary
方法,它拥有一个泛型类型T
的参数item
。为了能够在item
上调用summary
而不出现错误,我们可以在T
上使用 trait bounds 来指定item
必须是实现了Summarizable
trait 的类型:
pub fn notify<T: Summarizable>(item: T) {
println!("Breaking news! {}", item.summary());
}
trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于T
上的 trait bounds,我们可以传递任何NewsArticle
或Tweet
的实例来调用notify
函数。列表 10-13 中使用我们aggregator
crate 的外部代码也可以传递一个WeatherForecast
的实例来调用notify
函数,因为WeatherForecast
同样也实现了Summarizable
。使用任何其他类型,比如String
或i32
,来调用notify
的代码将不能编译,因为这些类型没有实现Summarizable
。
可以通过+
来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用T
类型的显示格式的同时也能使用summary
方法,则可以使用 trait bounds T: Summarizable + Display
。这意味着T
可以是任何实现了Summarizable
和Display
的类型。
对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的where
从句中。所以相比这样写:
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
{
这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。
使用 trait bounds 来修复largest
函数
所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的largest
函数定义了!当我们将其放置不管的时候,它会出现这个错误:
error[E0369]: binary operation `>` cannot be applied to type `T`
|
5 | if item > largest {
| ^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
在largest
函数体中我们想要使用大于运算符比较两个T
类型的值。这个运算符被定义为标准库中 trait std::cmp::PartialOrd
的一个默认方法。所以为了能够使用大于运算符,需要在T
的 trait bounds 中指定PartialOrd
,这样largest
函数可以用于任何可以比较大小的类型的 slice。因为PartialOrd
位于 prelude 中所以并不需要手动将其引入作用域。
fn largest<T: PartialOrd>(list: &[T]) -> T {
但是如果编译代码的话,会出现不同的错误:
error[E0508]: cannot move out of type `[T]`, a non-copy array
--> src/main.rs:4:23
|
4 | let mut largest = list[0];
| ----------- ^^^^^^^ cannot move out of here
| |
| hint: to prevent move, use `ref largest` or `ref mut largest`
error[E0507]: cannot move out of borrowed content
--> src/main.rs:6:9
|
6 | for &item in list.iter() {
| ^----
| ||
| |hint: to prevent move, use `ref item` or `ref mut item`
| cannot move out of borrowed content
错误的核心是cannot move out of type [T], a non-copy array
,对于非泛型版本的largest
函数,我们只尝试了寻找最大的i32
和char
。正如第四章讨论过的,像i32
和char
这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy
trait。当我们将largest
函数改成使用泛型后,现在list
参数的类型就有可能是没有实现Copy
trait 的,这意味着我们可能不能将list[0]
的值移动到largest
变量中。
如果只想对实现了Copy
的类型调用这些代码,可以在T
的 trait bounds 中增加Copy
!列表 10-15 中展示了一个可以编译的泛型版本的largest
函数的完整代码,只要传递给largest
的 slice 值的类型实现了PartialOrd
和Copy
这两个 trait,例如i32
和char
:
Filename: src/main.rs
use std::cmp::PartialOrd;
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("The largest char is {}", result);
}
Listing 10-15: A working definition of the largest
function that works on any generic type that implements the PartialOrd
and
Copy
traits
如果并不希望限制largest
函数只能用于实现了Copy
trait 的类型,我们可以在T
的 trait bounds 中指定Clone
而不是Copy
,并克隆 slice 的每一个值使得largest
函数拥有其所有权。但是使用clone
函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest
的实现方式是返回 slice 中一个T
值的引用。如果我们将函数返回值从T
改为&T
并改变函数体使其能够返回一个引用,我们将不需要任何Clone
或Copy
的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期(lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。