Rust 生命週期
簡介
生命週期對於許多 Rust 的初學者來說是一個很難理解的概念。它是一個新的概念,以至於大多數程序員從未在其他任何語言中見過它。在本文中,我們剖析生命週期的意義,並提供清晰識別生命週期的方法。
生命週期的目的
在討論細節之前,讓我們首先理解爲什麼存在生命週期,它們的作用是什麼?生命週期可以幫助編譯器執行一個簡單的規則:任何引用都不應該比它的值活得長。換句話說,生命週期幫助編譯器消除懸垂指針錯誤。編譯器通過分析所涉及的變量的生命週期來實現這一點。如果引用的生命週期小於值的生命週期,代碼就會編譯,否則就不會編譯。
“生命週期” 這個詞的含義
生命週期如此令人困惑的部分原因在於,在 Rust 的大部分寫作中,生命週期這個詞被輕率地用於指代三種不同的東西——變量的實際生命週期、生命週期約束條件和生命週期註釋。讓我們一個一個來看。
變量的生命週期
這是簡單的,變量的生命週期是指它存活的時間。這個意思最接近詞典中關於事物存在一段時間的含義。例如,在下面的代碼中,x 的生命週期會一直延伸到外部塊的末尾,而 y 的生命週期會在內部塊的末尾結束。
{
let x: Vec<i32> = Vec::new();//---------------------+
{// |
let y = String::from("Why");//---+ | x's lifetime
// | y's lifetime |
}// <--------------------------------+ |
}// <---------------------------------------------------+
生命週期約束條件
變量在代碼中的交互方式對它們的生命週期施加了一些約束。例如,在下面的代碼中,添加一個約束,x 的生命週期應該包含在 y 的生命週期內:
//error:`y` does not live long enough
{
let x: &Vec<i32>;
{
let y = Vec::new();//----+
// | y's lifetime
// |
x = &y;//----------------|--------------+
// | |
}// <------------------------+ | x's lifetime
println!("x's length is {}", x.len());// |
}// <-------------------------------------------+
如果沒有添加這個約束,println! 可以訪問 x,x 是 y 的引用,而 y 在上一行中會被銷燬。
請注意,約束不會改變實際的生命週期—例如,x 的生命週期仍然擴展到外部塊的末尾—它們只是編譯器用來禁止懸垂引用的工具。在上面的例子中,實際的生命週期不滿足約束條件:x 的生命週期已經超出了 y 的生命週期。因此,這段代碼無法編譯。
生命週期註釋
很多時候編譯器自動生成生命週期約束,但是隨着代碼變得更加複雜,編譯器會要求程序員手動添加約束,程序員通過生命週期註釋來實現這一點。例如,在下面的代碼片段中,編譯器需要知道 print_ret 函數返回的引用是否借用了 s1 或 s2,因此編譯器要求程序員顯式地添加這個約束:
//error:missing lifetime specifier
//this function's return type contains a borrowed value,
//but the signature does not say whether it is borrowed from `s1` or `s2`
fn print_ret(s1: &str, s2: &str) -> &str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();
let s1 = print_ret(&some_str, &other_str);
}
程序員需要用'a 註釋 s2 和返回的引用,從而告訴編譯器返回值是從 s2 借來的:
fn print_ret<'a>(
s1: &str,
s2: &'a str
) -> &'a str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();
let s1 = print_ret(&some_str, &other_str);
}
我想強調的是,僅僅因爲註釋'a 出現在參數 s2 和返回的引用上,並不意味着 s2 和返回的引用具有完全相同的生命週期。相反,這應該讀作:返回的帶有註釋'a 的引用是從具有相同註釋的實參借來的。
由於 s2 進一步借用了 other_str,生命週期的約束是:返回的引用不能比 other_str 長。代碼編譯通過是因爲確實滿足了生命週期約束:
fn print_ret<'a>(
s1: &str,
s2: &'a str
) -> &'a str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();//-------------+
let ret = print_ret(&some_str, &other_str);//---+ | other_str's lifetime
// | ret's lifetime |
}// <-----------------------------------------------+-----------------+
在展示更多示例之前,讓我簡要介紹一下生命週期註釋語法。要創建生命週期註釋,必須首先聲明生命週期參數。例如,<'a> 是生命週期聲明。生命週期參數是一種泛型參數,一旦聲明瞭生命週期參數,就可以在引用中使用它來創建生命週期約束。
記住,通過使用'a 註釋引用,程序員只是制定了一些約束;然後,編譯器的工作就是爲'a 找到滿足約束的具體生命週期引用。
例子
接下來,考慮一個函數 min,它找到兩個值的最小值:
fn min<'a>(
x: &'a i32,
y: &'a i32
) -> &'a i32 {
if x < y {
x
} else {
y
}
}
fn main() {
let p = 42;
{
let q = 10;
let r = min(&p, &q);
println!("Min is {}", r);
}
}
在這裏,'a 形參註釋了參數 x、y 和返回值。這意味着返回值可以從 x 或 y 中借用。由於 x 和 y 分別從 p 和 q 中借用,返回的引用的生命週期也應該包含在 p 和 q 的生命週期中。這段代碼可以編譯通過,因爲滿足了約束條件:
fn min<'a>(
x: &'a i32,
y: &'a i32
) -> &'a i32 {
if x < y {
x
} else {
y
}
}
fn main() {
let p = 42;//-------------------------------------------------+
{// |
let q = 10;//------------------------------+ | p's lifetime
let r = min(&p, &q);//------+ | q's lifetime |
println!("Min is {}", r);// | r's lifetime | |
}// <---------------------------+--------------+ |
}// <-------------------------------------------------------------+
通常,當函數有兩個或多個引用參數時,返回的引用生命週期不能超過生命週期最短的引用參數。
最後一個例子,許多新的 c++ 程序員都會犯返回局部變量指針的錯誤。在 Rust 中,類似的嘗試是不允許的:
//Error:cannot return reference to local variable `i`
fn get_int_ref<'a>() -> &'a i32 {
let i: i32 = 42;
&i
}
fn main() {
let j = get_int_ref();
}
由於 get_int_ref 函數沒有實參,編譯器知道返回的引用必須借用局部變量,這是不允許的。編譯器正確地避免了災難,因爲當返回的引用試圖訪問局部變量時,它將被清除:
fn get_int_ref<'a>() -> &'a i32 {
let i: i32 = 42;//-------+
&i// | i's lifetime
}// <------------------------+
fn main() {
let j = get_int_ref();//-----+
// | j's lifetime
}// <----------------------------+
消除規則
當編譯器允許程序員省略生命週期註釋時,稱爲生命週期省略。再說一遍,“生命週期省略” 這個術語是有誤導性的——當生命週期與變量的存在和消失不可分割地聯繫在一起時,它怎麼可能被省略呢?被省略的不是生命週期,而是生命週期註釋,以及擴展生命週期約束。在早期版本的 Rust 編譯器中,不允許省略,並且需要每個生命週期註釋。但是隨着時間的推移,編譯器團隊觀察到生命週期註釋的相同模式不斷重複。所以制定了消除規則。
在以下情況下,程序員可以省略註釋:
當只有一個輸入引用時。在這種情況下,將輸入的生命週期註釋分配給所有輸出引用。
例如:
fn some_func(s: &str) -> &str
fn some_func<'a>(s: &'a str) -> &'a str
當有多個輸入引用時,但第一個參數是 & self 或 & mut self。在這種情況下,第一個參數的生命週期註釋也被分配給所有輸出引用。
例如:
fn some_method(&self) -> &str
fn some_method<'a>(&'a self) -> &'a str
總結
變量的生命週期必須滿足編譯器和程序員對它們施加的某些約束,然後編譯器才能確保代碼是安全的。如果沒有生命週期機制,編譯器將無法保證大多數 Rust 程序的安全性。
本文翻譯自:
https://hashrust.com/blog/lifetimes-in-rust/
coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qyCWevEM8zgVxVv13c0nIA