Rust 讓人難以理解的 lifetime
本篇分享案例來自 The Rust Book[1], 在很多模糊的地方增加自己的理解
上次分享了 Rust 引用, 不熟悉的可以先回顧下前文。首先什麼是 lifetimes
? 生命週期定義了一個引用
的有效範圍,換句話說 lifetimes
是編譯器用來比對 owner 和 borrower 存活時間的工具,目的是儘可能的避免懸垂引用 (dangling pointer)
fn main() {
{
let r;
{
let x = 5;
r = &x;
// ^^ borrowed value does not live long enough
}
// - `x` dropped here while still borrowed
println!("r: {}", r);
// - borrow later used here
}
}
let r;
聲明瞭一個變量,在內層語句塊中變成對 x 變量的引用,當內層語句塊結束後,變量 x (owner) 脫離作用域釋放,println 時 r 成了懸垂引用,所以編譯器報錯
借用檢查器
借用檢查器 (borrow checker) 用來對比作用域,來決定這個引用是否有效。上圖有兩個註釋 'a
'b
來分別代表 r, x 的作用域,內存語句塊的 'b
遠遠小於外層的 'a
, 編譯階段發現 x 生命週期短於 r, 所以報錯阻止編譯
如果要修復也很簡單,把 println
放到內層語句塊即可。大多數時候,我們不需要顯示指定 lifetimes, 編譯器很智能,會自動幫我們推斷,但也有例外
看個例子
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
}
}
來看一個需要指定 lifetimes 的例子,longest
返回字符串最長的引用,編譯時報錯
編譯器蒙逼了,他不知道函數返回的引用到底是哪一個,需要指定生命週期。並且很貼心的給了提示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
這裏引出 lifetimes 的一個規則:如果函數輸入參數有引用 (凡是引用必然有 lifetimes), 返回結果如果不是引用,那麼可以省略標註生命週期,反之必須標註
道理很簡單,如果返回結果是引用,那麼根據 ownership 的三原則,他引用的對象一定不是函數內部創建的,因爲函數返回後,該引用的對象會被釋放掉,返回的引用就成了懸垂引用
所以,返回引用的生命週期必然和輸入參數的一致,這就引出 lifetimes 第二個規則:如果輸入參數只有一個是引用,帶有 lifetimes, 且返回值也是引用,那麼這兩個生命週期必然一致,可以省略標註 lifetimes, 反之必須標註
這裏稍微有些繞,大家需要仔細想想並且多跑跑測試例子,加深理解,我剛開始接觸這裏也走了很多彎路
生命週期語法
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
語法沒什麼特別的,就是泛型語法,通常從 'a
開始,'b
, 'c
都行,寫成別的也可以
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
上面的例子 <'a>
是泛型語法,函數簽名表示 x, y 生命週期是一樣的,那麼返回引用自然也是 'a
fn main() {
let s: &'static str = "I have a static lifetime.";
}
但是上面的靜態生命週期的要用 'static
關鍵字,表示該引用存活在整個程序運行期間。該字符串會被編譯到二進制 data 數據段中
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
}
}
上面是和正常泛型參數結合的例子,<'a, T>
, 第一個 'a
是生命週期,第二個是泛型 T
, 要求實現 Display
trait. 注意這裏面順序不能顛倒,如果 T
放到前面會報錯
error: lifetime parameters must be declared prior to type parameters
--> src/main.rs:6:36
|
6 | fn longest_with_an_announcement<T, 'a>(x: &'a str, y: &'a str, ann: T) -> &'a str
| ----^^- help: reorder the parameters: lifetimes, then types: `<'a, T>`
error: aborting due to previous error
函數簽名裏的標註
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);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
這個例子就會報錯,雖然我們指定了生命週期,但沒什麼用。string2
在語句塊結束後就被釋放了,println resut 時繼續使用 string2 的引用就是非法的。把 println 放在語句塊內部就可以了
多個生命週期參數
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
如果函數有多個生命週期參數,'a
, 'b
, 返回引用是 'a
, 此時編譯會報錯
11 | fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
| ------- -------
| |
| this parameter and the return type are declared with different lifetimes...
...
15 | y
| ^ ...but data from `y` is returned here
原理很簡單,編譯器無法確定這兩個 lifetimes 的有效長度,需要指定約束
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
最終函數如上所示,其中 'b: 'a
表示 'b
一定包括 'a
生命週期長度
結構體內的標註
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 };
}
在結構體定義時,如果存在引用,也要寫上 'a
註釋,泛型語法這裏不再贅述了。part
是一個字符串引用,在實例 i
創建前就存在了,並且和 i
同時離開作用域被釋放
如果去掉泛型的 lifetime 註釋,就會報錯
error[E0106]: missing lifetime specifier
--> src/main.rs:2:11
|
2 | part: & str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
結構體方法的標註
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
結構體方法的標註語法要在 impl
關鍵字後寫上 <'a>
, 並在結構體名後使用。這個例子中並沒有在 announce_and_return_part
中標註,這裏引出另一條規則 如果方法有多個輸入生命週期參數並且其中一個參數是 &self 或 &mut self,說明是個對象的方法 (method), 那麼所有輸出生命週期參數被賦予 self 的生命週期
假如把 announce_and_return_part
返回值換成 announcement
就會報錯
9 | fn announce_and_return_part(&self, announcement: &str) -> &str {
| ---- ----
| |
|this parameter and the return type are declared with different lifetimes...
...
12 |announcement
| ^^^^^^^^^^^^ ...but data from `announcement` is returned here
這時需要顯示的指定來協助編譯器來完成檢查,指定 lifetime. 另外涉及子類型,協變,逆變時生命週期會更復雜一些,感興趣的可以參考 nomicon[2] 官方文檔
小結
雜七雜八寫了一大堆,建議大家還是上手多練,多琢磨。以前剛學 rust 時,有人說編譯不過的話,生命週期就加 ''a'
, 數據就多用 clone
其實呢,**還是要理解本質,和 Go GC 運行期遍歷不同,檢查器要在編譯期確定資源何時何處釋放,就需要收集額外的信息。比如說,結構對象有個引用字段。如果無法確認它的生命週期,那麼結構對象釋放時,是否要釋放該字段?還是說該字段可以提前自動釋放,是否導致懸垂引用?**顯然,這違反了安全規則
再次強調,Rust 爲了所謂的零運行時成本,把很多 GC 語言的工作放到了編譯期
寫文章不容易,如果對大家有所幫助和啓發,請大家幫忙點擊在看
,點贊
,分享
三連
關於 Rust lifetime
大家有什麼看法,歡迎留言一起討論,大牛多留言 ^_^
參考資料
[1]
the rust book: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html,
[2]
subtyping-and-variance: https://doc.rust-lang.org/nomicon/subtyping.html#subtyping-and-variance,
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tX4ghbGuY3p25t1HofPCIg