Rust 生命周期
生命周期时一种泛型。普通泛型可以确保类型拥有期望的行为,生命周期能够确保引用在我们的使用过程中一直有效。
Rust的每个引用都有自己的生命周期,它对应着引用保持有效性的作用域。
在大多时候,生命周期都是隐式的且可以被推导出来的,就如同大部分时候类型也是可以被推导的一样。当出现了多种可能的类型时,我们就必须手动的声明类型。类似的,当引用的声明周期可能以不同的方式相互关联时,我们就需要手动标注生命周期,确保引用有效。
我的总结:当可能作为返回值的传入参数有多个引用,且没有使用self,且有多个引用与返回值有关,需要使用生命周期标注。
使用生命周期来避免悬垂引用
来看一份错误代码:
//错误代码 fn main() { { let r; { let x = 5; r = &x; } println!("r: {}", r); } }
明显报错了,引用r所指向的x的值已经在上面的作用域中销毁了。
Rust时如何去顶这段代码不合法的呢?我们来了解一下借用检查器。
借用检查器
//错误代码 fn main() { { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } // ---------+ }
r的生命周期标注为’a,x的生命周期标注为’b,如果被引用者的生命周期短于引用者,则不可引用。
(我觉得这里的表述有点奇怪)
正确的代码:
fn main() { { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ }
函数中的泛型生命周期
我们先写一个函数,比较两个字符串切片长度,返回最长的字符串切片。并且不获得所有权。
于是我们写了下面的这个错误代码:
//错误代码 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 } }
错误的原因是:longest可能返回x或y的引用,但是rust并不能确定最后是谁返回,因为借用检查器不知道x与y的生命周期是如何与返回值的生命周期相关联的,if语句块下面返回了x,else语句块下面返回了y,我们也不知道谁会被执行。所以我们需要添加一个生命周期泛型,使得借用检查器可以正常运行。
生命周期标注语法
在不影响生命周期的前提下,标注本身会被用于描述多个引用生命周期之间的关系。
可以这样声明生命周期标注:
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime
解释一下最后一个:拥有生命周期’a的指向i32的可变引用。
单个生命周期标注本身并没有多大意义,因为生命周期标注之所以存在就是为了向rust描述多个泛型生命周期参数之间的关系。比如参数first和参数second都有生命周期a’,那么first和second的引用必须和这里的泛型生命周期存活一样长的时间。
函数签名中的生命周期标注(重点,官方文档难理解)
接下来我们来为longest函数添加生命周期标注:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
因为生命周期也是泛型所以记得要在longest后面加<'a>。代码的函数签名向rust表明,函数所获取的两个参数的存活时间,必须不短于’a,且返回值也可以获取不短于a’的生命周期。
说了半天,那么我们定义的a’的生命周期到底是多长呢?
被与用于替代’a的具体生命周期就是x的作用域和y的作用域重叠的那一部分,来看一份错误代码:
//错误代码 fn main() { let string1 = String::from("xxxxx"); //-------------+---'x let result; // | { // | let string2 = "yyy".to_string(); //---+'y 'a | result = longest(string1 .as_str(), string2.as_str());// | | println!("The longest string is {}", result); // | | } //---+ | println!("{}",result); // | } //-------------+ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
返回值的result的生命周期原本是类似’x,调用了longest后result的生命周期并没有改变,但是使用result的合法范围发生了改变,因为借用检查器判断可能出现非法引用(longest返回的是string2的引用就非法了),所以println!("{}",result); 将会报错。
我的理解:声明周期标注在函数调用后通过参数,才会确定具体生命周期,生命周期是同标注的传入参数的交集,明确了生命周期后,传入给借用检查器。至始至终,并没有哪个变量的生命周期被改变,但是合法使用的周期却可能改变了。
我引入了一个合法周期的概念,区别于生命周期,是指不被借用检查器拒绝编译的生命周期。
深入理解生命周期
指定生命周期的方式往往取决于函数的具体功能。
来看一份代码:
fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
因为返回值与y完全没有关系,所以可以不指定y的生命周期。
再看一份错误代码:
fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); result.as_str() }
x和y都没有定义生命周期,所以’a无法确定生命周期,返回值也无法确定生命周期。
即使我们正确定义了生命周期标注,result也无法使用,因为,result的生命周期只到这个函数的尽头。
结构体定义中的生命周期标注
结构体中可以添加引用,指向其他变量。不过需要为结构体定义中的每一个引用都添加生命周期标签。
语法如下:
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, }; }
可以尝试分析i关于’a的生命周期。
生命周期省略
在没有显式标注生命周期的情况下,编译器目前使用了三种规则来计算引用的生命周期。这些规则不但对fn生效,也对impl生效。第一条用于输入,二三两条用于输出。
第一条规则:每一个引用参数都会拥有自己的生命周期参数。
fn foo<'a, 'b>(x: &'a i32, y: &'b i32);
第二条规则:当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命周期参数。
fn foo<'a>(x: &'a i32) -> &'a i32
第三条规则:
当拥有多个输入生命周期参数,而其中一个时&self或&mut self时,self的生命周期会被赋予给所有的输出生命周期参数。
方法定义中的生命周期标注
因为生命周期省略第三规则的存在,大部分时候写方法是可以不关注生命周期的。
但是生命在impl以及类型名称后面的生命周期标注是不能省略的。
impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } }
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part } }
静态生命周期
rust中还存在一种特殊的生命周期:'static
表示整个程序的执行期,所有的字符串字面量都拥有静态生命周期。
我们可以显式地写出来:
let s: &'static str = "I have a static lifetime.";
同时使用泛型,trait和生命周期
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {}", result); } 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 } }
哈希指的就是键值对。 创建一个哈希映射 下面的代码中创建了一个hash映射,rust推导了hashmap的类型: fn main() { use std::collections::HashMap; let mut ...