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.3 生命周期与引用有效性
Rust中的每一个引用都有其声明周期,也就是引用保持有效的作用域。它如大部分变量的类型一样是可以推断的,但有时候引用的生命周期关联方式不同,我们需要使用泛型声明周期参数来注明他们的关系,这样就能保证实际引用是有效的
生命周期避免了悬垂引用
fn main(){
let r;
{
let x = 5;
r = &x;
}
println!("r:{}",r);
}
出现错误
error[E0597]: `x` does not live long enough
--> src\main.rs:7:13
|
7 | r = &x;
| ^^ borrowed value does not live long enough
8 | }
| - `x` dropped here while still borrowed
9 | println!("r:{}",r);
| - borrow later used here
错误代码提示变量火的不够久,那Rust是怎么检查出来这个错误的呢?
借用检查器
借用检查器会比较作用域来确保所有的借用都是有效的
函数中的泛型生命周期
让我们比较一下两个字符串的长度
fn main(){
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(),string2);
println!("the longest string is {}",result)
}
fn longest(x:&str,y:&str)->&str{
if x.len()>y.len(){
x
}else{
y
}
}
但是此时还是不能编译,因为Rust并不知道返回x的引用还是y的引用,其实我们也不知道
生命周期注解语法
默认使用 ‘a
&i32
&’a i32
&'a mut i32
单个的生命周期注解本身没有多大意义,主要是描述多个引用生命周期相互的关系,而不影响其生命周期
函数签名中的生命周期注解
就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中,这里我们告诉Rust关于参数中的引用和返回值之间的限制是它们都必须拥有相同的生命周期,以下代码是能够让main函数编译的
fn longest<'a>(x:&'a str,y:&'a str)->&'a str{
if x.len()>y.len(){
x
}else{
y
}
}
这个程序中,我们告诉Rust函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。我们再来看一个直观的例子:
fn main(){
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("the longest string is {}",result)
}
}//the longest string is long string is long
我们再来看另外一个例子,运行时会报错
fn main(){
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("the longest string is {}",result)
}
//
error[E0597]: `string2` does not live long enough
--> src\main.rs:8:44
|
8 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
9 | }
| - `string2` dropped here while still borrowed
10 | println!("the longest string is {}",result)
| ------ borrow later used here
深入理解生命周期
指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将longest函数的实现修改为总是返回第一个参数而不是最长的的字符串slice,就不需要为参数y指定一个生命周期,如下代码能够编译:
fn longest<'a>(x:&'a str,y:&str)->&'a str{
x
}
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的longest函数实现
fn longest<'a>(x:str,y:&str)->&'a str {
let result = String::from("really long string");
result.as_str()
}
error[E0597]: `result` does not live long enough
--> src/main.rs:3:5
|
3 | result.as_str()
| ^^^^^^ does not live long enough
4 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'a as defined on the
function body at 1:1...
--> src/main.rs:1:1
|
1 | / fn longest<'a>(x: &str, y: &str) -> &'a str {
2 | | let result = String::from("really long string");
3 | | result.as_str()
4 | | }
| |_^
即使我们指定了生命周期参数‘a,编译还是失败,因为返回值的生命周期与参数完全没有关联
综上:生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联,一旦他们形成了某种关联,Rust就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为
结构体定义中的生命周期注解
之前,我们定义过所有权类型的注解。现在我们定义一个包含引用的结构体,不过要使用生命周期注解
struct ImportantExcerpt<'a> {
part:&'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.' ");
let i = ImportantExcerpt{part:first_sentence};
}
我们定义一个结构体,它里面只有一个字段part,并且类型是一个字符串slice的引用。我们在结构体后和字段类型的前都使用了生命周期参数,意味着ImportantExcerpt实例不能比part字段中的引用存在的更久
在main函数中,novel的作用域比ImportantExcerpt实例生命周期更长,因此这个引用是有效的
生命周期的省略
我们知道每一个引用都有一个生命周期,并且我们需要为了那些使用了引用的函数或者结构体指定生命周期。但下面的代码却能编译成功,为什么呢?
fn first_word(s:&str)->&str{
let bytes = s.as_byes();
for (i,&item) in bytes.iter().enumerate(){
if item == b' '{
return &s[0..i];
}
}
&s[..]
}
因为在早期版本中,这的确不是不能编译的,但随着Rust的不断完善,在特定情况下,这些生命周期注解场景是可预测的并且遵循几个明确的模式,Rust团队就把这些模式编码进了Rust编译器中。并且在未来之后将会有更少的注解
被编码进Rust引用分析的模式被称为生命周期省略规则
省略规则并不提供完整的推断:遇到模棱两可的情况,编译器会报错
函数或方法的参数生命周期被称为 输入生命周期,而返回值的生命周期被称为 输出生命周期
编译器采用三条规则来判断引用何时不需要明确的注解,第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则都没有计算出生命周期的引用,编译器将会停止并且生成错误,这些规则适用于fn定义,以及impl块
第一条规则:每个引用的参数都有它自己的生命周期参数,如:
fn foo<'a>(x:&'a i32)
fn foo<'a,'b>(x:&'a i32,y:&'b i32)
第二条规则:只有一个输入生命周期参数,那么它被赋予所有输出生命周期的参数:
fn foo<'a>(x:&'a i32)->&'a i32
第三条规则:输入生命周期参数有多个,其中一个是&self或&mut self。说明是个对象的方法(method)那么所有输出生命周期参数被赋予self的生命周期。第三条规则使得的方法更容易读写、因为只需更少的符号
现在我们假设自己是编译器,来根据三条规则检查如下代码
fn first_word(s: &str)->&str{
此引用没有关联任何生命周期
根据第一条规则,应该如下:
fn first_word<'a>(s: &'a str )-> &str{
根据第二条规则,应该如下:
fn first_word<'a>(s: &'a str )-> &‘a str{}
fn longest<'a,'b>(x:&'a str,y:&'b str)->&str
第三条规则我们看下面的内容
方法定义中的生命周期注解
impl<'a> ImportantExpert<'a>{
fn level(&self)-> i32{
3
}
}
impl之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标准self引用的生命周期
我们看一个适用于第三条规则的例子
impl<'a> ImportantExpert<'a>{
fn announce_and_return_part(&self,announcement:&str)->&str{
println!("Attention please:{}",announcement);
self.part
}
}
这里有两个输入生命周期,所以Rust应用第一条生命周期规则并给予&self和announcement他们各自的生命周期。接着,因为其中一个参数是&self,返回值类型就被赋予了&self的生命周期,这样所有的生命周期都被计算出来了
静态生命周期
我们再来讨论一种特殊的生命周期:‘static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有’static生命周期,如下
let s: &'static str = "I have a static lifetime.";
综合泛型类型参数、trait bounds和生命周期
use std::fmt::Display;
fn longest_with_an_announcement<'a,T>(x:&'a str,y:&'a str,ann:T)-> &'a str
where T:Display
{
println!("Announcement!{}",ann);
if x.len()>y.len(){
x
}else {
y
}
}
这将会返回两个字符串slice中较长者的longest函数,不过带有一个额外的参数ann,ann的类型是泛型T,它可以被放入任何实现了where从句中指定的Display trait的类型。这个额外的参数会在函数比较字符串slice的我长度之前被打印出来,这也就是为什么Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数‘a 和泛型参数T都位于函数名后的同一尖括号列表中
总结:本章我们所介绍的泛型、trait、生命周期都是为了简化代码。但是这个话题还有更多的内容,我们将会在后面章节中继续学习