标题
- 五、定义共享的行为
- 5.1 定义trait
- 5.2 实现trait
- 5.4 将trait作为参数
- 5.5 Trait Bound
- 5.6 返回实现了trait的类型
- 5.7 使用trait bounds修复get_max_value_from_vector的错误
- 5.8 使用trait bound有条件地实现方法
五、定义共享的行为
- 如果告诉Rust编译器某个
特定类型
拥有可能与其他类型共享的功能,可以通过trait
实现; - train类似于其他语言中的
接口
功能;
5.1 定义trait
- 一个类型的行为由其可供调用的方法构成;
- 对不同类型调用相同的方法,这些类型就可以共享相同的行为;
- trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合;
有两个存放了不同类型和属性文件的结构体
- 结构体
NewsArticle
用于存放发生于世界各地的新闻故事; - 结构体
Tweet
最多只能存放 280 个字符的内容,以及一些其他内容的元数据;
现存要创建一个多媒体聚合库用来显示可能储存在NewsArticle或Tweet 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summarize方法来请求总结。
pub trait Summary {
fn summarize(&self) -> String;
}
- 使用
trait
关键字进行定义,后面跟trait的名字; - 大括号内部声明这个trait的方法;
- 不需要写函数体,而直接跟上分号结尾;
- 也可以有多个方法声明,每个声明以分号结尾;
5.2 实现trait
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
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!("{}", tweet.summarize()); //horse_ebooks: of course, as you probably already know, people
}
- 采用
impl 结构体名 for trait名 {}
的方式实现; - 大括号内实现
trait内声明的所有行为
; - 实现trait后,就可以在main方法中在Tweet 实例中调用 trait 方法;
- 公有
pub
的trait可以使得其他crate可以实现此trait;
为trait增加默认实现
pub trait Summary {
fn summarize(&self) -> String{
String::from("(Read more...)")
}
}
impl Summary for NewsArticle{}
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...)
}
代码所做的改变如下
- 为Summary trait 的
summarize
方法指定一个默认的字符串值; - 将NewsArticle对该trait的实现置空
(如果不置空调用的时候不会采用默认的实现)
;
5.4 将trait作为参数
- 定义一个函数调用参数item上的
summarize
方法,该参数是实现了Summary trait的某种类型; - 使用impl Trait语法实现
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
- 对于item参数,指定了impl关键字和trait名称,而不是具体的类;
- item参数支持任何实现了指定trait的类型;
- 在notify函数体中,可以调用任何来自Summary trait的方法(比如 summarize);
- 可以传递任何NewsArticle或Tweet的实例来调用notify;
- 任何用其它如 String 或 i32 的类型调用该函数的代码都不能编译,因为它们没有实现 Summary;
5.5 Trait Bound
- impl Trait语法适用于直观的例子,它实际上是一种较长形式的语法糖,称之为
trait bound
,看起来像
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
- trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面;
- impl Trait适用于短小的例子,trait bound则适用于更复杂的场景;
例如获取两个实现了Summary的参数,使用impl Trait语法实现
pub fn notify(item1: impl Summary, item2: impl Summary) {}
- 上述代码适用于item1和item2允许是不同类型的情况,如果强制它们都是相同的类型呢?那就只能使用trait bound
pub fn notify<T: Summary>(item1: T, item2: T) {]
- 泛指类型T被指定为item1和item2,因此这两个参数的具体类型必须一致
通过 + 指定多个trait bound
- notify需要显示item的格式化形式,同时也要使用summarize方法;
- 因此item就需要同时实现两个不同的trait:Display和Summary;
- 可以通过
+
语法实现; - 通过指定两个trait bound,nofity的函数体就可以调用summarize并使用{}格式化item;
pub fn notify(item: impl Summary + Display) {}
- 也适用于泛型的trait bound
pub fn notify<T: Summary + Display>(item: T) {}
通过where简化trait bound
- 每个泛型有其自己的 trait bound;
- 有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息;
- 这就会使得函数参数多到难以阅读;
- Rust可以在函数之后的where从句中指定trait bound的语法;
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}
等价于
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
5.6 返回实现了trait的类型
可以在返回值中使用impl Trait语法来返回实现了某个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,
}
}
- 指定了
returns_summarizable
函数返回某个实现了 Summary trait 的Tweet,但是调用方并不知情; - 返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用;
- 闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型;
- impl Trait允许简单的指定函数返回一 Iterator而无需写出实际的冗长的类型;
上述只适用于返回单一类型的情况,要返回不同的类型就无法编译通过;
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,
}
}
}
编译报错
5.7 使用trait bounds修复get_max_value_from_vector的错误
在 get_max_value_from_vector函数体中我们想要使用大于运算符(>)比较两个T类型的值。这个运算符被定义为标准库中 trait std::cmp::PartialOrd 的一个默认方法。所以需要在 T 的 trait bound 中指定 PartialOrd,这样get_max_value_from_vector函数可以用于任何可以比较大小的类型的 slice。因为 PartialOrd 位于 prelude 中所以并不需要手动将其引入作用域。将get_max_value_from_vector函数修改如下
fn get_max_value_from_vector<T: PartialOrd>(src: &[T]) -> T {
let mut max = src[0];
for &item in src.iter(){
if item > max {
max = item;
}
}
max
}
编译报错
- i32 和 char 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 Copy trait;
- 当我们将 get_max_value_from_vector函数改成使用泛型后,现在src参数的类型就有可能是没有实现 Copy trait 的。这意味着我们可能不能将 src[0] 的值移动到max变量中,这导致了上面的错误;
- 为了只对实现了 Copy 的类型调用这些代码,可以在 T 的 trait bounds 中增加 Copy;
fn get_max_value_from_vector<T: PartialOrd + Copy>(src: &[T]) -> T {}
然而
- 代码实现在功能上没问题;
- 数据对比时增加值的拷贝功能会增大程序的运行时长,如果是String类型则增加的运行时长更加多;
- 最好的解决方法是直接返回最大值的引用,这样就避免了拷贝操作;
最终代码
fn get_max_value_from_vector<T: PartialOrd>(src: &[T]) -> &T {
let mut max = &src[0];
for item in src.iter(){
if item > max {
max = item;
}
}
max
}
fn main() {
let x = vec![12, 21, 78, 56, 77];
let y = vec![11.5, 100.3, 1.0, 90.1];
println!("max x: {}", get_max_value_from_vector(&x)); //max x: 78
println!("max y: {}", get_max_value_from_vector(&y)); //max y: 100.3
}
5.8 使用trait bound有条件地实现方法
- 通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
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);
}
}
}
- 类型Pair<T>总是实现了 new 方法,不过只有那些为T类型实现了PartialOrd trait允许比较和Display trait启用打印的Pair<T>才会实现 cmp_display 方法;
也可以对任何实现了特定 trait 的类型有条件地实现 trait
- 对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations;
- 他们被广泛的用于 Rust 标准库中;
标准库为任何实现了 Display trait 的类型实现了ToString trait,这个 impl 块看起来像这样
impl<T: Display> ToString for T {
// --snip--
}
由于标准库有了这些 blanket implementation,我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值(因为整型实现了 Display)
let s = 3.to_string()