10 泛型、trait和生命周期
每一种编程语言都有高效处理重复概念的工具,Rust使用的是泛型。泛型是具体类型或其它抽象类型的替代。我们可以表达泛型的属性,比如他们的行为如何与其它类型的泛型相关联,而不许需要在编写或者编译代码时知道它们在这里实际上代表的是什么
我们编写一个函数,想让它对多种类型的参数都具有处理能力,而不仅仅是针对某种具体类型的参数定义函数。这个时候我们就可以指定函数的参数类型是泛型,而不是某种具体类型。我们之前使用了Option、Vec!和HashMap<K,V>
提取函数来减少重复
在此之前,我们先来回顾一个不使用泛型就解决代码重复的技术,提取函数
fn main() {
let number_list = vec![34,25,50,100,65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number
};
};
println!("the largest number is {}",largest)
}
我们先遍历了一个vector,求其元素的最大值,那如果我们要遍历另一个元素的话,就需要再重复一下这些代码,现在我们用函数来做个提取
fn largest(list:&[i32])->i32{
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!("{}",result)
}
通过函数抽象,我们就不用再重用代码了,只需要调用这个比较逻辑(我们新定义好的函数)即可
但是!我们在这里寻找的是vec中的最大值,如果我们要寻找char、slice中的最大值呢?下面我们来着手解决这个问题
10.2 trait:定义共享的行为
trait告诉编译器某个特定的类型可能拥有与其他了类型共享的功能。因此我们可以使用trait来抽象定义一组共享的行为,trait有点像其它语言中的接口。可以使用trait bounds指定泛型是任何拥有特定行为的类型
定义trait
一个类型的行为由其可供调用的方法构成,如果不同的类型可以调用相同的方法,那这些类型就共享这种相同的行为.而trait定义就是将这种方法签名组合起来的方法,而它的目的就是定义一个行为集合,这些行为可以实现我们的某个目的
我们来看一个例子:有两个结构体NewsArticle 和 Tweet ,前者存放新闻故事,后者存放元数据,类似转推或回复。但我们现在想要创建一个多媒体聚合库,它显示的是NewsArticle 或 Tweet中的数据总结。每个结构体都对应一个行为(方法),这样,我们就可以定义一个 trait
pub trait Summary {
fn summarize(&self)->String;
}
关于定义trait的语法格式,非常简单。使用关键字trait + trait名称,然后花括号中写具体实现的方法,可以写一个也可以写多个,并且每个都以分号结尾
为类型实现trait
我们把上面的案例再来剖析下,然后在这个两个struct上实现trait,使用impl + trait名称 + for + 具体类型名称 语法格式
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.location,self.author)
}
}
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)
}
}
一旦实现了trait,我们就可以调用了
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
不过这边有几个注意事项需要提醒一下:
当定义的trait和类型是不在同一作用域时,使用trait要先将其引入作用域。假如上述Summary trait 和NewsArticle、Tweet都在礼包。rs中定义。并且lib.rs在aggregator crate下,如果别人想要使用这个crate的功能时,需要用use aggregator::Summary; 来引入。也就是说只有当trait或者和要实现trait的类型位于crate的本地作用域时,才能为该类型实现trait
但是不能为外部类新实现外部trait。例如不能在aggregator 中为Vec实现Display trait。这是因为Display和Vec都定义与标准库中,它们并不位于aggregator crate本地作用域中。这个限制被称为相干性的程序属性的一部分,或者更具体的说是孤儿规则。其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你的代码,,反之亦然,没有这条规则的话,两个crate分别对相同的类型实现相同的trait,而Rust无从得知应该使用哪一个实现
默认实现
有时候为trait里的方法提供默认行为,而不是针对每个具体类型都定义自己的行为很有用。这样,为某个特定类型实现trait时,可以选择保留或者重载每个方法的默认行为
如下为Summary trait的summarize指定了一个默认的字符串值,而不是像实例中那样只定义方法签名
pub trait Summary {
fn summarize(&self)->String{
String::from("Read more...")
}
}
如果想要对Tweet实例使用这个默认实现,可以通过impl Summary for Tweet {} 指定一个空的impl块,如下:
impl Summary for 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:Read more...
我们再来看一个比较有趣的调用,之前我们有提到一个trait中可以定义多个类型的方法(也可以是默认的方法),这个有意思的点是在实际操作中,我们可以trait中的一个方法调用另一个方法
pub trait Summary {
fn summarize_auther(&self)->String;
fn summarize(&self)->String{
format!("(Read more from {}...)",self.summarize_auther())
}
}
impl Summary for Tweet {
fn summarize_auther(&self) -> String{
format!("@{}",self.username)
}
}
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:(Read more from @horse_ebooks...)
注意:无法从相同方法的重载实现中调用默认方法
trait 作为参数
同样的,trait也可以作为函数的参数!我们来看看例子,trait 作为参数类型是写作 impl trait名称,如下,函数体内可以调用trait内的任何方法,即只要是实现了Summary的类型都可以
pub trait Summary {
fn summarize_auther(&self)->String;
fn summarize(&self)->String{
format!("(Read more from {}...)",self.summarize_auther())
}
}
pub fn notify(item:impl Summary){
println!("Breaking news! {}",item.summarize())
}
Trait bound 语法
impl Trait 实际上是一种语法糖,称为trait bound,它看起来像:
pub fn notify<T:Summary>(item:T) {
println!("Breaking news! {}",item.summarize());
}
这与之前的例子相同,不过稍微冗长了一些,trait bound与泛型参数声明在一起,位于尖括号中的冒号后面
impl trait很方便,适用于简单的例子,trait bound则适用更复杂的场景,以下是impl trait写法:
pub fn notify(item1:impl Summary,item2:impl Summary){...}
这里的item1和item2允许是不同的类型,如果希望强制它们都是相同的类型,只能用 trait bound
pub fn notify<T:Summary>(item1:T,item2:T){...}
通过+ 指定多个 trait bound
item要实现两个trait
pub fn notify(item: impl Summary + Display) {...}
泛型的trait bound 也可以 +
pub fn notify<T:Summary + Dispaly>(item:T) {...}
通过指定这两个trait bound,notify 的函数体可以调用summarize 并使用{} 来格式化item
通过where 简化trait bound
使用过多的trait bound会让代码变的难以阅读,我们可以用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
}
返回实现了trait的类型
trait 既然可以作为参数类型,那当然可以作为返回值类型,这样,函数就会返回一个实现了Summary trait的类型,但是不能确定具体类型是什么,只知道这个类型实现了Summary trait, 调用方也并不知情
这点在闭包和迭代器的场景中十分有用,后面我们会介绍它,闭包和迭代器创建只有编译器知道的类型,或者非常非常长的类型,impl trait允许你简单的指定函数返回一个Iterator而无需写出实际冗长的类型
fn return_summariable()->impl Summary{
Tweet{
username: String::from("horse_ebooks"),
content:String::from("of course, as you probably already know, people"),
reply:false,
retweet:false,
}
}
不过这只适合返回单一类型的情况,如下这种返回NewsArticle 或 Tweet就无法编译,这是因为impl trait 工作方式的限制,后面我们会在讲“为使用不同类型的值而设计的trait对象”部分会介绍如何编译这样一个函数
fn returns_summarizable(switch:bool) -> impl Summary {
if switch {
NewsArticle {
headline:String::from("Penguins win the Staley Cup Championship!"),
location:String::from("Pittshburgh,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,
}
}
}
使用 trait bounds 来修复largest 函数
我们来看看原来的例子:
fn main(){
let number_list = vec![34,50,25,100,65];
let result = largest(&number_list);
println!("{}",result);// 100
let char_list = vec!['y','m','a','q'];
let result = largest(&char_list);
println!("{}",result)// y
}
运行输出如下结果:
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src\main.rs:36:17
|
36 | if item > largest{
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
32 | fn largest<T: std::cmp::PartialOrd>(list:&[T]) -> T{
在largest 函数体中我们想要使用 > 来比较两个T类型的值,这个运算符是定义于标准库 trait std::cmp::PartialOrd的一个默认的方法,所以需要在T的trait bound 中指定 PartialOrd,这样largest 函数可以用于任何可以比较大小的类型的slice。因为PartialOrd位于preclude中所以并不需要手动将其引入作用域,修改largest 的签名
fn largest<T:PartialOrd>(list:&[T]) -> {}
但是如果编译代码的化,会出现一些不同的错误
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
| help: consider using a reference instead: `&list[0]`
error[E0507]: cannot move out of borrowed content
--> src/main.rs:4:9
|
4 | 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 slice
这是因为像 i32 和 char这种类型实现了copy trait(已知大小并可以存储在栈上),但是largest函数改成使用泛型后,现在list的参数就有可能没有实现copy trait,所以我们可能不能将list[0]的值移动,这就导致上面的错误
我们可以只调用实现了copy的类型,如下是可以编译的代码
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 number_list = vec![34,50,25,100,65];
let result = largest(&number_list);
println!("The largest vnumber is {}",result);
let char_list = vec!['y','m','a','q'];
let result = largest(&char_list);
println!("The largest char is {}",result)
}
//
The largest vnumber is 100
The largest char is y
如果并不希望限制largest 函数只能用于实现了Copy trait的类型,我们可以在T的trait bounds中指定clone而不是Copy。并克隆slice的每个值使得largest函数拥有其所有权,使用clone函数意味着对于类似于String这样拥有堆上数据的类型。会潜在分配更多堆上空间而堆分配在涉及大量数据时可能会相当缓慢
另一种largest的实现方式是返回在slice中T值的引用,如果我们讲函数返回值从T改为&T并改变函数体使其能返回一个引用。我们将不需要任何clone或Copy的trait bounds而且也不会有任何堆分配
使用trait bound 有条件的实现方法
通过使用带有trait bound的泛型参数的impl块,可以有条件的只为那些实现了特定trait类型实现方法,如下
struct Pair<T> {
x:T,
y:T,
}
impl<T> Pair<T>{
fn new(x:T,y:T) -> Self{
Self {
x,
y,
}
}
}
impl<T:Dispaly + 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);
}
}
}
也可以对任何实现了特定trait的类型有条件的实现trait,对任何满足特定trait bound 的类型实现trait被称为 blanket implementations。它们被广泛的用于Rust标准库中。例如,标准库为任何实现了display trait的类型实现了ToSorting trait,这个impl 块看起来像
impl<T:Display> ToString for T {...}
因为标准库有了这些blanket implementation,我们可以对任何实现了Display trait 的类型调用由ToString 定义的to_string 方法。例如:可以将整型转换为对应的String值,因为整型实现了Display:
let s = 3.to_string();
blanket implementation 会出现在trait 文档的“Implementers"部分
trait和trait bound 让我们使用泛型类型参数来减少重复,但是也告知了编译器泛型类型应拥有哪些行为,编译器会检查具体类型是否提供了正确的行为,这种检错机制是很好的
还有一种泛型是声明周期,不同于其它泛型,确保类型拥有期望的行为,而生命周期则有助于我们引用时,这些类型一直有效