泛型、trait、生命周期
泛型数据类型
可以在声明函数签名或结构体、枚举等元素时使用泛型
在函数定义中
在函数名后跟一个<>
,里面放上类型参数的声明。(类似于C++中的 template<class T>
)
之后在函数的定义时,使用这些类型参数即可
比如实现一个为多个类型切片选择最大值的函数:
fn largest<T>(list : &[T]) -> T{
let mut l = list[0];
for &item in list.iter() {
if item > l {
l = item;
}
}
l
}
此时还不能通过编译,这在我们之后讲trait时讨论
在使用时,可以不显式的指明类型参数,让编译器自己推导
let nums = vec![1,2,3];
let l = largest(&nums);
在结构体定义中
同样的,在结构体名后跟一个<>
,里面放上类型参数的声明
在之后的结构体定义时,使用这些类型参数
比如实现一个任意类型坐标的点
struct Point<T,U> {
x:T,
y:U,
}
let p = Point {x:10,y:4.0};
在枚举定义中
同样的,在枚举名后跟一个<>
,里面放上类型参数的声明
在之后的枚举定义时,使用这些类型参数
比如我们的Result枚举
enum Result<T,E> {
Ok(T),
Err(E),
}
在方法定义中
我们在impl后跟一个<>
,表示在这个impl块内起作用的泛型参数
在impl块内进行定义的方法也可以有自己的泛型参数
比如
impl<T,U> Point<T,U> {
fn mixup<V,W>(self,other:Point<V,W>) -> Point<T,W> {
Point {
x:self.x,
y:other.y,
}
}
}
我们也可以为某些泛型的特例定义方法
impl<T> Point<T,i32> {
....
}
这里面的方法就仅仅为y的参数类型为i32的Point类型所拥有
泛型代码的性能问题
无需为泛型的使用付出任何运行时的代价,这是因为Rust会在编译时执行泛型代码的单态化
单态化时,编译器寻找所有泛型代码调用过的地方,并基于其所使用的类型生成具体类型的代码
这有点类似于,将下面的代码
let int = Some(5);
let float = Some(1.0);
变为
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
let int = Option_i32::Some(5);
let float = Option_f64::Some(1.0);
所有的开销都是编译期间的
trait:定义共享行为
trait与其它语言的interface很类似。在Rust中,trait用来向编译器描述某些特定类型拥有且能够被其他类型共享的功能
还可以用trait约束来讲泛型参数指定为实现了某些特定行为的类型
定义trait
使用trait 特征名 {定义体}
来定义一个trait
在定义体中是一些函数签名,函数签名直接用;
分隔
// src/lib.rs
pub trait Summary {
fn summerize(&self) -> String;
}
为类型实现trait
使用impl trait名 for 类型名 {为tari中的所有函数签名提供定义}
来为一个类型实现trait
// src/lib.rs
pub struct Tweet{
...
}
impl Summary for Tweet {
fn summarize(&self) -> String {
...
}
}
实现trait有一个限制,只有当trait或类型的定义在我们的库中,我们才能进行实现trait。
- 可以为当前库的类型实现外部trait,只需使用use引入就好,比如为Tweet类型实现Display trait;这也是为什么我们在定义trait时使用pub。
- 可以为外部库的类型实现定义在我们库里的tarit,比如可以为Vec实现我们库的Summary trait
- 不能为外部库的类型实现外部库的trait,比如不能在我们的程序中为Vec实现Display trait
默认实现
可以为某些或者所有方法提供默认行为,当我们为某个类型实现trait时,可以选择保留(就是在impl块内不再重复定义该方法)或者重载(重新定义该方法)
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read More...)")
}
}
impl Summary for Article {}
使用trait作为参数
在参数类型中写impl trait名
,代表任何实现了指定trait的类型
这种参数的显著特点就是可以调用trait约定的方法
pub fn notify(item: impl Summary) {
println!("Breaking news! {}",item.summarize());
}
该函数就可以接收任何实现了Summary trait的类型示实例,比如说Tweet类型
trait约束
impl trait名
只是trait约束的一个语法糖,用于简短的实例中
trait约束是在泛型的声明中指定trait,像是<T:trait>
,代表该类型参数是“实现了指定trait的类型”
pub fn notify<T:Summary>(item:T) {
通过+语法来指定多个trait约束
多个trait约束之间使用+
来进行连接
pub fn notify(item: impl Summary+Display) {
pub fn notify<T:Summary+Display>(item:T) {
使用where从句来简化trait约束
在函数签名之后使用where trait约束
来使得函数签名显得不那么杂乱(其语义就像英语的定语从句一样)
fn f<T:Display+Clone,U:Clone+Debug>(t:T,u:U) ->i32 {
换为
fn f<T,U>(t:T,u:U) ->i32
where T:Display+Clone,
U:Clone+Debug
{
返回实现了trait的类型
在返回值中使用impl Trait语法 fn return_summarizable() -> impl Summary {
- 为什么?与后面的闭包和迭代器有关
- 只能在该函数只能返回某一特定类型时使用,比如如果该函数既能返回Tweet又有可能返回Article,就不能使用impl Summary作为返回值;但是如果该函数只可能返回Tweet一种类型,使用impl Summary作为返回类型就是合法的
使用 trait 约束有条件的实现方法
同样的,上面函数是在函数的泛型声明中加入trait约束
在impl块的泛型声明中同样也可以加入trait约束,表示只为特定实现了trait的类型定义方法
use::std::fmt::Display;
struct Pair<T> {
x:T,
y:T,
}
impl<T:Display+PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("the largest numer is x:{}",self.x);
} else {
println!("the largest numer is y:{}",self.y);
}
}
}
比如该块就是只为Pair的字段类型是实现了Display和PartialOrd的类型实现cmp_display方法
覆盖实现
为满足trait约束的所有类型实现另外的trait称为覆盖实现(与普通的为特定类型trait实现相比,类似于批量实现trait)
比如标准库中为所有实现了Display trait的类型实现ToString trait
impl<T:Display> ToString for T {
//...
}
使用生命周期保证引用的有效性
每个引用都有自己的生命周期,它对应引用保持有效性的作用域
借用检查器
借用检查器用于在编译时期比较不同的作用域并确定所有借用的合法性
给出一个例子介绍其大致原理:
如上图,变量r
的生命周期标注为'a
,变量x
的生命周期标注为'b
。比较,得到'b
小于'a
,即被引用对象的存在范围短于引用者,拒绝通过编译
上面代码的最终结果就是产生一个悬垂引用
函数中的泛型生命周期
查看下面这个比较两个字符切片长度并返回较长那个的函数
fn longer(x:&str,y:&str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
该函数定义通过不了编译,报错会提示我们需要给返回类型标注一个泛型生命周期参数
这是因为,我们在定义函数时,既不会知道传入引用的具体生命周期,也不知道返回的引用会指向x还是y,所以借用检查器无法通过简单的作用域比较来确定返回的引用是否有效
生命周期标注语法
- 单个生命周期标注没有意义
- 标注是用于描述多个泛型生命周期
- 必须以单引号开头,通常全部使用小写字母
- 标注在&后面,比如
&'a mut i32
函数名中的生命周期标注
在<>
内声明泛型生命周期,并在函数的声明标签中进行使用,就完成了一次生命周期标注
fn longer<'a>(x:&'a str,y:&'a str) -> &'a str {
就像泛型参数<T>
是在具体的参数传入时被确定并完成单态化那样,
泛型生命周期参数<'a>
也在具体的参数传入时,被替代为所有被标记为'a
的引用参数中(在这里是x
和y
)生命周期较短的那一个的生命周期
在编译时,就会检查返回值中被标记为'a
的引用的生命周期是否小于等于实例化的'a
代表的生命周期,如果否,就不通过编译
比如给出一个不通过编译的方式:
fn main() {
le string1 = "1".to_str();
let result;
{
let string2 = "2".to_str();
result = longer(&strng1,&string);
}
}
在上面的例子中,根据传入的参数,'a
被实例化为string2
的生命周期。而返回值引用result
的生命周期大于string2
的生命周期,所以未通过编译
深入理解生命周期
从根本上说,生命周期语法就是用来关联一个函数中不同参数和返回值的生命周期的。
当函数返回一个引用时,返回类型的生命周期必须要与其中一个参数的生命周期相匹配
结构体定义中的生命周期标注
在结构体中储存引用时,要为结构体定义中的每一个引用添加生命周期标注
仍然是在就结构体名后<>
内声明泛型生命周期参数
struct Foo<'a> {
x: &'a i32,
}
该生命周期的工作方式也类似于“泛型”。当创建实例时,该泛型生命周期'a
被实例化为该结构体实例的生命周期,之后编译器就会检查结构体引用字段中所有标记为'a
的其生命周期是否大于等于该实例的生命周期,如果否,拒绝通过编译
在方法定义中的生命周期标注
为拥有生命周期的结构体声明方法时,必须在impl块的声明处标识生命周期
impl<'a> Foo<'a> {
至于具体方法,如果还需要生命周期参数,则按照函数泛型生命周期参数的规则进行声明
生命周期省略
函数参数或方法参数中的是生命周期被称为输入生命周期,返回值的生命周期被称为输出生命周期
在没有显式的标注下,应用以下三条规则来进行生命周期的计算
- 每一个引用参数拥有一个独特的生命周期参数
- 只存在一个输入生命周期参数时,该生命周期参数被赋予所有输出生命周期参数
- 当拥有多个输入生命周期参数,而其中一个是
&self \ &mut self
时,self
的生命周期会被赋予所有输出生命周期参数
比如
fn f(s:&str) -> &str {
根据1、2条,这样的函数声明是合法的
静态生命周期
'static
表示整个程序的执行期
所有字符串字面量都有'static
生命周期
慎用该生命周期,确保声明为'static
的引用真的是在程序运行期间持续有效