Rust 生命週期
生命周期
为何需要有生命周期
先看一个例子:我们要求有一个函数,这个函数能够返回两个字符串中比较长的哪一个,比如下面的代码就要求输出 abcdefg
。
fn main() {
let x: String = String::from("abcdefg");
let y: String = String::from("abcd");
println!("{}", longest(x.as_str(), y.as_str()));
}
似乎不难,因为下面这样就可以了
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
return x;
}
y
}
但真的可以吗?如果使用 IDE 这时候已经报错了,即便没有使用 IDE,当你尝试编译的时候编译器也会告诉你:
error[E0106]: missing lifetime specifier
--> src\main.rs:1:35
|
1 | fn longest(x: &str, y: &str) -> &str {
| ----- ----- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `demo` due to previous error
大概是说希望你指明生命周期(lifetime),但是那是个什么东西?好吧,为了解释这个问题,在此之前我们先修改一下 main
函数中的例子。改成下面这样。
fn main() {
let x: String = String::from("abcdefg");
let result: &str;
{
let y = String::from("abcd");
result = longest(x.as_str(), y.as_str());
}
println!("{}", result);
}
你会发现依旧无法通过编译,比不过这个就很好理解了。因为 longest 返回一个 str 的引用,但是根据 longest 的逻辑它即可能返回 x 的引用,也有可能返回 y 的引用。在我们修订过后的例子 x 和 y 的作用域不相同,假如返回 y 的引用,这时候是万万不得行的——你可以想想在 Golang 中闭包外使用闭包内的变量吗?也就是说,如果返回的是 y 的引用那么就会出现 result 还有效,但是 y 已经离开作用域了。如果此时允许上面的程序通过编译,result 就会指向一个不存在的引用,也就变成了野指针(悬垂指针)。对编译器来说,它并不知道你会返回哪一个,所以无法判断是否会出现野指针,所以它不允许你通过编译。
解决办法
之所以无法让上述代码通过编译根本目的是为了避免野指针,而根本原因是编译器无法判断参数和返回值之间的作用域到底有多大。而 Rust 的解决办法是,虽然我不知道,但是写代码的程序员知道,由他告诉我不久可以了吗?具体的做法就是如同提示新息中那样,将函数签名修改成如下样子:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
return x;
}
y
}
如此 longest 便能够通过编译并正确执行第一个例子了。
理解生命周期
这个写法啥意思呢?尖括号中的 ‘a
表示的就是生命周期,而在参数列表中 x 和 y 在声明数据类型的时候都加上了 ’a
意思就是 x 和 y 拥有相同的生命周期,都是 'a
。诶,你可能会疑问,哪我是不是可以将他们生命为不同类型的生命周期呢?当然可以,比如下面这样:
fn foo<'a, 'b>(x: &'a str, y: &'a str) {}
如此 x 和 y 便拥有了不同的生命周期。是不是越来越明显了?是的,这 TM 不就是泛型的语法吗?是的,没错,all right。这就是泛型的语法,因为**生命周期就是泛型的一种(《Rust 权威指南 》 P273)。**但是你可能会不理解,既然是泛型的一种,哪为何还要独立出来呢?这里不妨思考一下他们希望解决的本质问题。泛型一开始就是为了解决代码复用问题,而生命周期呢?解决的是作用域的问题(准确来说是参数和返回值作用域的关系问题)。所以,你可以将生命周期视作泛型的扩展,而从根本来说,生命周期的语法是用来关联一个函数中不同参数以及返回值作用域的。
Rust 所谓的生命周期在很大程度上与其他语言的作用域的概念很相似,但确实不是同一个概念。而本文多次使用作用域的说法,甚至看到有意无意之间混淆两者之间的界线,这只是便于理解,望悉知。
我们前面说,生命周期是为了让程序员告诉编译器不同参数和返回值作用域之间的关系,但是并没有说生命周期让不同参数和返回值拥有相同的生命周期。**这是因为生命周期的语法并不会改变参数和返回值的生命周期,只是要求他们拥有相同的生命周期。**最简单的来说你会发现上面的 longest 确实能够正确执行第一个例子,但是第二个修改过后的例子就变得无法通过编译。这是因为修改过后的例子中 x,y 拥有不同的生命周期,而函数 longest 要求他们具备相同的生命周期。所以 x,y 不是函数 longest 的合法参数。这里也就引出了生命周期的第4个要点:生命周期是用于帮助编译器检查程序的语法,而非改变参数属性,所以在参数传递上并没有出现变化 。
小结:
- 生命周期是一种辅助编译器检测的语法。
- 生命周期是泛型的一种。
- 生命周期是用来声明函数参数和返回值作用域关系的一种语法。
- 生命周期语法不会改变参数和返回值的作用域(生命周期)。
生命周期省略
你是不是想到了下面这个例子:
fn get_string(t: &str)-> &str {
String::from("hello").as_str()
}
这个例子中函数 get_string
接收一个 &str 并且返回一个 &str,而参数和返回值之间并没有通过生命周期进行关联,但是你发现上面的代码依旧无法通过编译。这是为何呢?因为这个函数其实是有生命周期的,只是被省略了,或者说他拥有省略生命周期。编译器会对省略命周期检查,检查规则有三:
- 每一个借用参数都各自拥有一个生命周期。
- 只有一个借用参数的时候,返回值生命周期与唯一的借用参数保持相同。
- 当第一个参数是
&self
,&mut self
的时候,其他参数和返回值与一个参数的生命周期保持相同。
我们依次套用上面三条规则对 get_string 进行检查。
规则 1:适用,所以代码变成如下
fn get_string<'a, 'b>(t: &'a str)-> &'b str {
String::from("hello").as_str()
}
规则 2:满足,所以变成如下
fn get_string<'a>(t: &'a str)-> &'a str {
String::from("hello").as_str()
}
规则 3:不满足,不做变化。所以代码最终如下
fn get_string<'a>(t: &'a str)-> &'a str {
String::from("hello").as_str()
}
这里也就不难看出问题了,函数签名要求返回值和 t 拥有相同的生命周期(作用域),但是实际上返回值 String::from("hello").as_str()
是在函数内部声明,而参数 t 由外部传如,生命周期(作用域)显然不同。而这也就是无法通过编译的原因。所以不难看出,省略生命周期意在简化代码编写过程。那么第三条如何理解呢?不急,往后看。
结构体与生命周期
既然我们已经说了生命周期是泛型的一种,那么,在结构体中使用生命周期也就变得信手拈来:
struct Man<'a> {
name: &'a str,
age: u8,
}
生命周期 & 方法
只是定义带有生命周期的结构体实际意义还不够,我们再来为其实现一个方法,如下:
impl<'a> Man<'a> {
fn rename(&mut self, name: &'static str) {
self.name = name
}
}
fn main() {
let name:String = String::from("tan");
let mut m: Man = Man{name: &name, age:13};
m.rename("ant");
println!("{}", m.name);
}
rename 为 Man 提供了一个修改姓名的方法。但是这似乎并没有带来什么不同,就连结构体实例化都与没有生命周期时别无二致。那么看下面两个例子:
例子 1:可以正常通过编译并执行。
fn main() {
let mut m: Man = Man{name:"ant", age:13};
{
let name = String::from("tan");
m.name = name.as_str();
}
// println!("{}", m.name);
}
例子 2:无法通过编译并执行。
fn main() {
let mut m: Man = Man{name:"ant", age:13};
{
let name = String::from("tan");
m.name = name.as_str();
}
println!("{}", m.name);
}
这是为什么呢?因为没有输出语句的时候 m,name 的生命周期都到内部的语句块结束就结束了,所以他们生命周期相同。但是当有输出语句的时候,m 的生命周期被延长到内部的语句块之外,但是这个时候 name 已经结束生命周期,入土为安了。所以第二段无法通过编译。所以此处生命周期在结构体中的意义也就体现了出来——保证结构体中每一个子段在结构体实例有效期都可以正常访问。
所以我们不妨修改一下我们原本的 rename 方法,让他更灵活一些
impl<'a> Man<'a> {
fn rename(&mut self, name: &'a str) {
self.name = name
}
}
这样我们便可以通过其他变量来 rename 而无需通过固定字符串。
静态生命周期
上面我们用到了一个我们不曾讲过的东西 'static
,他表示的是静态生命周期,既在整个程序运行其间都能被有效访问。而字符串便是其中之一(HelloWorld
这样的字符串,而不是 String``::``**from**``(``"tan"``)
这样的 String 类型)。
說在最後
最後說一些比較雜碎的內容:
只針對引用
生命週期只針對借用(引用),對於對於轉移並不有效。比如下面兩個修改過的 longest 函數實現
fn longest_1(x: String, y: String) -> String {
if x.len() > y.len() {
return x;
}
y
}
fn longest_2(x: &str, y: &str) -> String {
if x.len() > y.len() {
return String::from(x);
}
String::from(y)
}
這兩個函數都是合法且正確的。之所以正確那是因爲檢查檢查生命週期這一機制是由借用檢查器完成。而借用檢查器並不會檢查轉移的變量——或者說對於轉移的變量其生命週期也被一併轉移,導致其原本的所有者無法使用,所以並不需要擔心野指針的存在。別忘了,生命週期的存在是爲了防止出現野指針。
只檢查函數簽名
對於下面的函數依舊是不合法的:
fn longest_3(x: &str, y: &str) -> &str {
x
}
其實非常明顯的是返回值的生命週期一定就是 x 的生命週期,但是這依舊不會通過編譯。因爲借用檢查器並不關心函數實現,只關心函數簽名。也就是,當函數簽名無法通過檢查的時候就是無法通過編譯,與函數體無關。
借用返回值必須與參數關聯
仔細想想,如果一個函數想要返回一個借用,那麼這個借用可以來自於什麼地方呢——全局變量,參數,局部變量。但是全局變量並不需要通過返回值的方式返回,只需要在外部直接調用即可,而局部變量則是在函數執行完成之後自動銷毀。因此如果返回一個借用,那麼這個借用一定和參數是有關聯的。所以借用檢查器當且僅當返回值爲一個借用的時候會被觸發,並且其要求返回值生命週期來自於其中一個參數。