Rust入坑指南:朝生暮死
今天想和大家一起把我們之前挖的坑再刨深一些。在Java中,一個物件能存活多久全靠JVM來決定,程式設計師並不需要去關心物件的生命週期,但是在Rust中就大不相同,一個物件從生到死我們都需要掌握的很清楚。
在Rust入坑指南:核心概念一文中我們介紹了Rust的幾個核心概念:所有權(Ownership)、所有權轉移和所有權借用。今天就來介紹Rust中的另外一個核心概念:生命週期。
為什麼生命週期要單獨介紹呢?因為我在這之前一直沒搞清楚Rust中的生命週期引數究竟是怎麼一回事。
現在我終於弄明白了,於是迫不及待要和大家分享,當然如果我有什麼說的不對的地方請幫忙指正。
在Rust中,值的生命週期與作用域有關,這裡你可以結合所有權一起理解。在一個函式內,Rust中值的所有權的範圍即為其生命週期。Rust通過借用檢查器對值的生命週期進行檢查,其目的是為了避免出現懸垂指標。這點很容易理解,我們通過一段簡單的程式碼來看一下。
fn main() {
let a; // 'a ---------------+
{ // |
let b = 1; // 'b ----+ |
a = &b; // | |
}// ---------------------+ |
println!("a: {}", a); // |
} // ----------------------------+
在上面這段程式碼中,我已經標註了a和b的生命週期。在程式碼的第5行,b將所有權出借給了a,而在第7行我們想使用a時,b的生命週期已經結束,也就是說,從第7行開始,a成為了一個懸垂指標。因此這段程式碼會報一個編譯錯誤。
而當所有權在函式之間傳遞時,Rust的借用檢查器就沒有辦法來確定值的生命週期了。這個時候我們就需要藉助生命週期引數來幫助Rust的借用檢查器來進行生命週期的檢查。生命週期引數分為顯式的和隱式的兩種。
顯式生命週期引數
顯式生命週期的標註方式通常是'a
這樣的。它應該寫在&
之後,mut
之前(如果有)。
函式簽名中的生命週期引數
在正式開始學習之前,我們還要先明確一些概念。下面是一個代有生命週期引數的函式簽名。
fn foo <'a>(s: &'a str, t: &'a str) ->&'a str;
其中第一個'a
,是生命週期引數的宣告。引數的生命週期叫做輸入宣告週期,返回值的生命週期叫做輸出生命週期。需要記住的一點是:輸出的生命週期長度不能長於輸入的生命週期。
另外還要注意:禁止在沒有任何輸入引數的情況下返回引用。因為這樣明顯會造成懸垂指標。試想當你沒有任何輸入引數時返回了引用,那麼引用本身的值在函式返回時必然會被析構,返回的引用也就成了懸垂指標。
同樣的道理我們可以得出另一個結論:從函式中返回一個引用,其生命週期引數必須與函式的引數相匹配,否則,標註生命週期引數也毫無意義。
說了這麼多“不允許”之後,我們來看一個正常使用生命週期引數的例子吧。
fn the_longest<'a> (s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let s1 = String::from("Rust");
let s1_r = &s1;
{
let s2 = String::from("C");
let res = the_longest(s1_r, &s2);
println!("{} is the longest", res);
}
}
我們來看看這段程式碼的各個值的生命週期是否符合我們前面說的那一點原則。在呼叫th_longest函式時,兩個引數的生命週期已經確定,s1的生命週期貫穿了main函式,s2的生命週期在內部的程式碼塊中。函式返回時,將返回值繫結給了res,也就是說返回的生命週期為res的生命週期,由於後定義先析構的原則,res的生命週期是短於s2的生命週期的,當然也短於s1的生命週期。因此這個例子符合了我們說的輸出的生命週期長度不能長於輸入的生命週期的原則。
對於像示例當中有多個引數的函式,我們也可以為其標註不同的生命週期引數,但是編譯器無法確定兩個生命週期引數的大小,因此需要我們顯式的指定。
fn the_longest<'a, 'b: 'a> (s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
這裡'b: 'a
的意思是'b
的存活週期長於'a
。這點有些令人疑惑,'a
明明是長於'b
的,為什麼會這樣標註呢?還記得我們說過生命週期引數的意義嗎?它是用來幫助Rust借用檢查器來檢查非法借用的,輸出生命週期必須短於輸入生命週期。因此這裡的'a
實際上是返回值的生命週期,而不是第一個輸入引數的生命週期。
函式中的生命週期引數的使用我們暫時先介紹到這裡。生命週期在其他使用場景中的使用方法也比較類似,不過還是有一些值得注意的地方的。
結構體中的生命週期引數
如果一個結構體包含引用型別的成員,那麼結構體應該宣告生命週期引數<'a>
。這是為了保證結構體例項的生命週期應該短於或等於任意一個成員的生命週期。
struct ImportantExcept<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("call me Ishmael. Some year ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcept { part: first_sentence};
assert_eq!(i.part, "call me Ishmael");
}
在這段程式碼中first_sentence
先於結構體例項i
被定義,因此i
的生命週期是短於first_sentence
的,如果反過來,i
的生命週期長於first_sentence
即長於part
,那麼在part
被析構以後,i.part
就會成為懸垂指標。
方法中的生命週期引數
現在我們為剛才的結構體增加一個實現方法
impl<'a> ImportantExcept<'a> {
fn get_first_sentence(s: &'a str) -> &'a str {
let first_sentence = s.split('.')
.next()
.expect("Could not find a '.'");
first_sentence
}
}
因為ImportantExcept
包含引用成員,因此需要標註生命週期引數。在impl
後面宣告生命週期引數<'a>
在結構體名稱後面使用。在get_first_sentence
方法中使用的生命週期引數也是剛剛定義好的那個。這樣就可以約束輸入引用的生命週期長度長於結構體例項的生命週期長度。
靜態生命週期引數
前面聊的都是我們自己定義的生命週期引數,現在來聊聊Rust中內建的生命週期引數'static
。'static
生命週期存活於整個程式執行期間。所有的字串字面量都有'static
生命週期,型別為&'static str
。
隱式生命週期引數
在某些情況下,我們可以省略生命週期引數,對於省略的生命週期引數通常有三條規則:
- 每個輸入位置上省略的生命週期都將成為一個不同的生命週期引數
- 如果只有一個輸入生命週期的位置,則該生命週期將分配給輸出生命週期
- 如果存在多個輸入生命週期的位置,但是其中包含&self或&mut self,則self的生命週期將分配給輸出生命週期
生命週期限定
生命週期引數也可以像trait那樣作為範型的限定
- T: 'a:表示T型別中的任何引用都要“活得”和'a一樣長
- T:Trait + 'a:表示T型別必須實現Trait這個trait,並且T型別中的任何引用都要“活得”和'a一樣長
總結
現在我把我對Rust生命週期的瞭解都分享完了。其實只要記住一個原則就可以了,那就是:生命週期引數的目的是幫助借用檢查器驗證引用的合法性,避免出現懸垂指標。
Rust還有幾個深坑,我們下次繼續