Traits:定义共同的行为——特性
特性告诉Rust编译器特定类型具有的功能,并可以与其他类型共享。我们可以使用特性以抽象的方式定义共享行为。我们可以使用特征范围来指定泛型可以是具有特定行为的任何类型。
注意:特性与其他语言中通常称为接口的功能相似,但有一些区别。
定义一个特性
类型的行为包含我们可以在该类型上调用的方法。 如果我们可以在所有这些类型上调用相同的方法,则不同的类型具有相同的行为。 特性定义是一种将方法签名分组在一起以定义实现某些目的所需的行为的方法。
例如,假设我们有多种结构,可容纳各种类型和数量的文本:NewsArticle结构可容纳在特定位置归档的新闻故事,而Tweet最多可包含280个字符以及指示它是否为 新推文,转发推文或对另一条推文的回复。
我们想要制作一个媒体聚合器库,以显示可能存储在NewsArticle或Tweet实例中的数据摘要。 为此,我们需要每种类型的摘要,并且需要通过在实例上调用summary方法来请求该摘要:/src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
在这里,我们使用trait关键字声明一个特征,然后使用该特征的名称,在本例中为Summary。 在大括号内,我们声明方法签名,这些签名描述实现此特征的类型的行为,在这种情况下为fn summarize(&self) -> String
。
在方法签名之后,我们使用分号代替在大括号内提供实现。 实现此特征的每种类型必须为方法主体提供其自己的自定义行为。 编译器将强制要求具有摘要特征的任何类型都必须使用此签名准确定义方法摘要。
一个特征可以在其主体中包含多种方法:方法签名每行列出一个,并且每行以分号结尾。
在类型上实现特征
现在,我们已经使用Summary特性定义了所需的行为,我们可以在媒体聚合器中的类型上实现它。下例显示了NewsArticle结构上Summary特性的实现,该特性使用标题,作者和位置来创建summary的返回值。 对于Tweet结构,假设tweet内容已限制为280个字符,我们将summary定义为用户名,然后是tweet的整个文本。/src/main.rs
pub trait Summary {
fn summarize(&self) -> String;
}
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 Traits for Object {
比如我们使用一下上例:
fn main() {
let tweet = Tweet {
username: String::from("电子书"),
content: String::from(
"当然,就像你所知道的一样,人们",
),
reply: false,
retweet: false,
};
println!("一条新消息: {}", tweet.summarize());
}
运行结果为:
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target\debug\cargo_learn.exe`
一条新消息: 电子书: 当然,就像你所知道的一样,人们
默认实现
有时,对特征中的某些或所有方法具有默认行为很有用,而不是要求对每种类型的所有方法都实施。 然后,当我们在特定类型上实现特征时,我们可以保留或覆盖每个方法的默认行为。
如下例所示,显示了如何为Summary trait的summary方法指定默认字符串,而不是像清上例中那样仅定义方法签名:
pub trait Summary {
fn summarize(&self) -> String {
String::from("默认返回值")
}
}
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)
}
}
fn main() {
let article = NewsArticle {
headline: String::from("一条推送消息"),
location: String::from("地址地址"),
author: String::from("作者"),
content: String::from(
"默认内容xxx, \
标题内容"
),
};
println!("推送: {}", article.summarize());
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target\debug\cargo_learn.exe`
推送: 一条推送消息, by 作者 (地址地址)
如果此时将impl Summary for NewsArticle {
内容置空:impl Summary for NewsArticle {}
,那么其运行结果为:
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target\debug\cargo_learn.exe`
推送: 默认返回值
默认实现可以调用具有相同特征的其他方法,即使其他方法没有默认实现也是如此。 通过这种方式,特征可以提供许多有用的功能,而仅要求实现者指定其一小部分。 例如,我们可以定义摘要特征以使其具有需要实现的summary_author方法,然后定义一个具有默认实现的summary方法,该默认实现调用summary_author方法:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(阅读更多来自{} ……)", 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)
}
}
fn main() {
let tweet = Tweet {
reply: false,
retweet: false,
username: String::from("作者zuoz"),
content: String::from("默认内容xxx, 默认内容"),
};
println!("推送: {}", tweet.summarize());
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target\debug\cargo_learn.exe`
推送: (阅读更多来自@作者zuoz ……)
特性作为参数
特征也可以作为参数,其实说白了就是抽取了上述示例的一部分作为一个公用方法而已:
fn main() {
let tweet = Tweet {
reply: false,
retweet: false,
username: String::from("作者zuoz"),
content: String::from("默认内容xxx, 默认内容"),
};
//println!("推送: {}", tweet.summarize());
fn notify(item: &impl Summary) {
println!("测试测试:{}", item.summarize());
}
notify(&tweet);
}
运行结果:
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target\debug\cargo_learn.exe`
测试测试:(阅读更多来自@作者zuoz ……)
我们指定了impl关键字和特征名称,而不是item参数的具体类型。 此参数接受实现指定特征的任何类型。 在notify主体中,我们可以调用来自Summary特性的item上的任何方法,例如summary。 我们可以在NewsArticle或Tweet的任何情况下致电通知并传递。 不能以其他任何类型(例如String或i32)调用该函数的代码,因为这些类型未实现Summary
。
特征约束语法impl Trait
语法适用于简单的情况,但实际上是较长形式(称为特征绑定)的语法糖。 它看起来像这样:
pub fn notify(item: &T) {
println!("测试测试:{}", item.summarize());
}
这和上例方法运行的结果是一样的。
impl Trait
语法很方便,并且在简单的情况下可以使代码更简洁。 在其他情况下,特征绑定语法可以表示更多的复杂性。 例如,我们可以有两个实现Summary
的参数。 使用impl Trait
语法如下所示:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
和明显item1和item2可以使两种不同的类型,但是都可以直接在其上调用Summary方法。如果如果两个对象的类型一样的话,那么将是长这样的:
pub fn notify(item1: &T, item2: &T) {
指定为item1和item2参数类型的通用类型T约束了函数,使得作为item1和item2的参数传递的值的具体类型必须相同。
用+语法来指定使用多个特征
我们还可以指定多个特征绑定。 假设我们要通知在item
上使用显示格式以及summarize
方法:我们在通知定义中指定项目必须同时实现Display
和Summary
。 我们可以使用+语法来做到这一点:pub fn notify(item: &(impl Summary + Display)) {
+语法对泛型类型的特征范围也有效:pub fn notify
复杂的实现关系可以使用 where 关键字简化,例如:
fn some_function(t: &T, u: &U) -> i32 {
可以简化成:
fn some_function(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
该函数的签名不太混乱:函数名称,参数列表和返回类型并排在一起,类似于没有很多特征界限的函数。
返回实现特征的类型实例
我们还可以在返回位置使用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)
}
}
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用于返回类型,我们指定return_summarizable函数返回某种实现Summary特性的类型,而无需命名具体类型。 在这种情况下,turns_summarizable返回一条Tweet,但是调用此函数的代码不知道这一点。
但是,仅当返回单一类型时,才可以使用impl Trait。 例如,此代码返回的NewsArticle或Tweet的返回类型指定为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 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。
修复实现最大值问题:
之前使用泛型获取最大值的时候报错:
$ cargo run
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
= note: `T` might need a bound for `std::cmp::PartialOrd`
在获取最大值的正文中,我们想使用大于(>)运算符比较两个T类型的值。 因为该运算符被定义为标准库特征std::cmp::PartialOrd
的默认方法,所以我们需要在T的特征范围中指定PartialOrd,以便最大的函数可以作用于我们可以比较的任何类型的切片。 我们不需要把PartialOrd纳入研究范围,因为它处于序幕中。 更改最大的签名,如下所示:
fn largest(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
但是在运行的时候:
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
--> src\main.rs:2:23
|
2 | let mut largest = list[0];
| ^^^^^^^
| |
| cannot move out of here
| move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
| help: consider borrowing here: `&list[0]`
error[E0507]: cannot move out of a shared reference
--> src\main.rs:4:18
|
4 | for &item in list {
| ----- ^^^^
| ||
| |data moved here
| |move occurs because `item` has type `T`, which does not implement the `Copy` trait
| help: consider removing the `&`: `item`
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `cargo_learn`.
To learn more, run the command again with --verbose.
此错误中的关键行不能移出非复制切片[T]类型。 对于最大函数的非泛型版本,我们只是试图找到最大的i32或char。 众所周知,可以将大小已知的类型(如i32和char)存储在堆栈中,因此它们实现了Copy特性。 但是当我们使最大的函数通用时,list参数中就有可能实现不实现Copy特征的类型。 因此,我们无法将值移出list[0]并移到最大变量中,从而导致此错误。
要仅使用实现Copy特征的那些类型来调用此代码,我们可以将Copy添加到T的特征范围!下例显示了一个通用的最大函数的完整代码,只要我们传递给该函数的slice中的值的类型实现PartialOrd和Copy特性,就可以编译该代码,如i32和char do:
fn largest(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
D:\learn\cargo_learn>cargo run
Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.70s
Running `target\debug\cargo_learn.exe`
The largest number is 100
The largest char is y
使用特质界限有条件地实施方法
通过使用与使用通用类型参数的impl块绑定的特征,我们可以为实现指定特征的类型有条件地实现方法。 例如,下例中的Pair
类型始终实现新功能。 但是Pair
仅在其内部类型T实现可实现比较的PartialOrd
特性和可进行打印的Display
特性时,才实现cmp_display
方法。
use std::fmt::Display;
struct Pair {
x: T,
y: T,
}
impl Pair {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl Pair {
fn cmp_display(&self) {
if self.x >= self.y {
println!("较大的数是:x = {}", self.x);
} else {
println!("较大的数是:y = {}", self.y);
}
}
}
对于任何实现了另一个特征的类型,我们也可以有条件地实现一个特征。 满足特征界限的任何类型的特征实现都称为一揽子实现,并在Rust标准库中广泛使用。 例如,标准库在实现Display
特征的任何类型上实现ToString
特征。 标准库中的impl
块看起来类似于以下代码:
impl ToString for T {
// --snip--
}
因为标准库具有这种全面的实现,所以我们可以在实现了Display
特征的任何类型上调用ToString
特征定义的to_string
方法。 例如,我们可以像这样将整数转换为其对应的String
值,因为整数实现了Display
:
impl ToString for T {
// --snip--
}
因为标准库具有这种整体实现,所以我们可以在实现Display特性的任何类型上调用ToString特性定义的to_string方法。 例如,我们可以像这样将整数转换为其相应的String值,因为整数实现了Display:let s = 3.to_string();