生命周期消除
實際上,對于編譯器來說,每一個引用類型都有一個生命周期,那么為什么我們在使用過程中,很多時候無需標注生命周期?例如:
fn first_word(s: &str) -> &str {let bytes = s.as_bytes();for (i,&item) in bytes.iter().enumerate() {if item == b' ' {return &s[0..i];}}&s[..]
}
該函數的參數和返回值都是引用類型,盡管我們沒有顯式的為其標柱生命周期, 編譯依然可以通過。 其實原因不復雜,編譯器為了簡化用戶的使用,運用來生命周期消除大法。
對于first_word 函數, 它的返回值是一個引用類型,那么該引用只有兩種情況:
? ? ? ?1.從參數獲取
? ? ? ? 2.從函數體內新創建的變量獲取
如果是后者,就會出現懸垂引用,最終被編譯器拒絕,因此只剩一種情況:返回值的引用是獲取自參數。 這就意味著參數和返回值的生命周期是一樣的。 道理很簡單,我們能看出來,編譯器自然也能看出來,因此。就算我們不標注生命周期,也不會產生其一。
實際上,在Rust 1.0 版本之前。這種代碼果斷不給通過,因為Rust 要求必須顯式的為所有引用標注生命周期
fn first_word<'a>(s: &'a str) -> & 'a str {
在些來大量的類似代碼后,Rust社區抱怨聲四起,包括開發者自己都忍不了了,最終揭鍋而起,這才有來我們今日的幸福。
生命周期消除的規則不是一蹴而就,而是伴隨著 總結-改善 流程的周而復始,一步一步走到今天,這也意味著,該規則以后可能也會進一步增加,我們需要手動標注生命周期的時候也會越來越少。?
在開始之前有幾點需要注意
? ? ? ? 1.消除規則不是萬能的, 若編譯器不能確定某件事是正確時, 會直接判為不正確。那么你還是需要手動標注生命感周期
? ? ? ? 2.函數或者方法中,參數的生命周期被成為? 輸入生命周期,返回值的生命周期 被成為 輸出生命周期
三條消除規則
編譯器使用三條消除規則來確定哪些場景不需要顯式地去標注生命周期,其中第一條規則應用在輸入生命周期上,第二,三條應用在輸出生命周期上,若編譯器發現三條規則都不適用時,就會報錯, 提示你需要手動標注生命周期。
? ? ? ? 1.每一個引用參數都會獲得獨自的生命周期
? ? ? ? 例如一個引用參數的函數就有一個生命周期標注: fn foo<'a>(x: &'a i32),? 兩個引用參數的有兩個生命周期標注 fn foo<'a,'b>(x: &'a i32, y: &'b i32) 依此類推
? ? ? ? 2.若只有一個輸入生命周期(函數參數中只有一個引用類型) ,那么該生命周期會被賦給所有的輸出生命周期, 也就是所有返回值的生命周期都等于該輸入生命周期。
? ? ? ? 例如函數 fn foo(x: &i32) -> &i32, x 參數的生命周期會被自動賦給返回值,&i32,? 因此該函數等同于 fn foo<'a>(x: &'a i32) -> &'a i32
? ? ? ? 3. 若存在多個輸入生命周期,且其中一個是&self 或 &mut self ,則 &self 的生命周期被賦給所有的輸出生命周期
? ? ? ??擁有 &self? 形式的參數,說明該函數是一個 方法, 該規則讓方法的使用便利度大幅提升。?
規則其實很好理解,但是,愛是靠的讀者肯定要發問來, 例如第三條規則,若一個方法,它的返回值的生命周期就是跟參數 &self 的不一樣怎么辦? 總不能強迫我返回的值總是和 &self 活得一樣久吧? 答案很記得那年: 手動標注生命周期,因為這些規則知識編譯器發現你沒有標注生命周期時默認去使用的, 當你標注生命周期后, 編譯器自然會乖乖聽你的話。
讓我們假裝自己是編譯器,然后看下以下的函數該如何引用這些規則:
例子1?
fn first_word(s: &str) -> &str { // 實際項目中的手寫代碼
首先,我們手寫的代碼如上所示時,編譯器會想引用第一條規則, 為每個參數標注一個生命周期:?
fn first_word<'a>(s: &'a str) -> &str{ // 編譯器自動為參數添加生命周期
此時,第二條規則就可以進行應用,因為函數只有一個輸入生命周期,因此該生命周期會被賦予所有的輸出生命周期:?
fn first_word<'a>(s: &'a str) -> &'a str{ // 編譯器自動為返回值添加生命周期
此時,編譯器為函數簽名中的所有引用都自動添加來具體的生命周期,因此編譯通過,且用戶無需手動去標注生命周期,只要按照 fn? first_word(s: &str) -> &str { 的形式寫代碼即可。
例子2
fn longest(x: &str,y: &str) -> &str { // 實際項目中的手寫代碼
首先,編譯器會引用第一條規則,為每個參數都標注生命周期:?
fn longest<'a,'b>(x:&'a str ,y: &'b str) -> &str {
但是此時,第二條規則卻無法被使用,因為輸入生命周期有兩個,第三條規則也不符合,因為它是函數,不是方法,因此沒有 &self 參數。 在挑用所有規則后,編譯器依然無法為返回值標注合適的生命周期。因此,編譯器就會報錯,提示我們需要手動標注生命周期。
error[E0106]: missing lifetime specifier
--> src/main.rs:1:47
|
1 | fn longest<'a, 'b>(x: &'a str, y: &'b 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`
note: these named lifetimes are available to use
--> src/main.rs:1:12
|
1 | fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
| ? ? ? ? ? ?^^ ?^^
help: consider using one of the available lifetimes here
|
1 | fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'lifetime str {
| ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?+++++++++
? 不得不說,Rust 編譯器真的很強大,還貼心的給我們提示來該如何修改,雖然。。。好像。。。 。它的提示貌似不太準確,這里我們希望參數和返回值都是 'a 生命周期。
方法中的生命周期
先來回憶下泛型的語法:?
struct Point<T> {x: T,y: T,
}impl<T> Point<T> {fn x(&self) -> &T {&self.x}}
實際上,為具有生命周期的結構體實現方法時,我們使用的語法跟泛型參數語法很相似:?
struct ImportantExcerpt<'a> {part: &'a str,
}impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}
其中有幾點需要注意的:
? ? ? ? 1.impl 中必須使用結構體的完整名稱, 包括 <'a> , 因為生命周期標注也是結構體類型的一部分!
? ? ? ? 2.方法簽名中,往往不需要標注生命周期,得益于生命周期消除的第一和第三規則?
下面的例子展示來第三規則引用的場景:
impl<'a> ImportantExcerpt<'a> {fn announce_and_return_part(&self , announcement: &str) -> &str{println!("Attention please: {}",announcement);self.part}
}
首先,編譯器引用第一規則,給予每個輸入參數一個生命周期:?
impl<'a> ImportantExcerpt<'a> {fn announce_and_return_part<'b>(&'a self,announcement: &'b str) -> &str {println!("Attention please:{} ",announcement);self.part}
}
需要注意的是, 編譯器不知道 announcement 的生命周期帶地多長,因此它無法簡單的給予它生命周期 'a , 而是重新聲明了一個全新的生命周期 'b .?
接著,編譯器應用第三規則額,將 &self 的生命周期賦給返回值 &str :
impl<'a> ImportantExcerpt<'a> {fn announce_and_return_aprt<'b> (&'a self, announcement: &'b str) -> 'a str{println!("Attention please: {}",announcement);self.part}
}
最開始的代碼,盡管我們沒有給方法標注生命周期,但是在第一和第三規則的配合下,編譯器依然完美的為我們亮起來綠燈。
在結束這塊內容之前,再來做一個有趣的修改, 將方法返回的生命周期改為'b :?
impl<'a> ImportantExcerpt<'a> {fn announte_and_return_part<'b>(&'a self,announcement: & 'b str) -> &'b str {println!("Attention please: {}",announcement);self.part}
}
此時編譯器會報錯,因為編譯器無法知道 'a 和 'b 的關系,&self? 生命周期是'a? ,那么 self.part 的生命周期也是? 'a , 但是好巧不巧的是,我們手動為返回值 self.part 標注來生命周期 'b, 因此編譯器知道 'a? 和 'b 的關系。?
有一點很容易推理出來: 由于 &'a self 是被引用的一方,因此引用它的&'b str 必須要活得比它端,否則會出現懸垂引用。 因此說明生命周期 'b 必須要比 'a 小,只要滿足來這一點,編譯器就不會在報錯
impl<'a: 'b,'b> ImportantExcerpt<'a> {fn announce_and_return_part(&'a self,announcement: &'b str) -> &'b str{println!("attention please: {}",announcement);self.part}
}
一個復雜的玩意被甩到來你面前,就問怕不怕?
就關鍵點稍微解釋下:
? ? ? ? 1.‘a: 'b, 是生命周期約束語法, 跟泛型越說非常顯式,用于說明'a 必須比'b 活得九
? ? ? ? 2.可以把 'a 和 'b 都在同一個地方聲明:(如上),或者分開聲明 但通過where 'a:'b 約束生命周期關系
impl<'a> ImportantExcerpt<'a> {fn announce_add_return_part<'b>(&'a self,announcement: &'b str) -> &'b str where 'a: 'b,{println!("Attention please: {}",announcement);self.part}
}
總之,實現方法比想象中簡單,: 加一個約束,就能暗示編譯器,盡管引用吧, 反正我先引用的內容比我活得久
靜態生命周期
在Rust中有一個非常特殊的生命周期,那就是 'static ,擁有該生命周期的引用可以和整個程序活得一樣久。
在之前我們學過字符串字面量, 提到過它是被硬編碼進Rust的二進制文件中,因此這些字符串變量全部具有 'static 的生命周期
let s: &'static str = "我沒啥優點,就是活得久,";
這時候,有些聰明的小腦瓜就開始動來, 當生命周期不知道怎么標時,對類型施加一個靜態生命周期的約束 T : 'static 是不是很爽? 這樣我和編譯器再也不用操心它到底活多久來,
嗯,只能說,這個想法是對的,在不少情況下, 'static 約束哦確實可以解決生命周期編譯不通過的問題, 但是問題來了,: 本來該引用沒有活那么久,但是你非要說它活那么久,玩意引入了潛在的Bug 怎么辦?
因此遇到因為生命周期導致的編譯不通過問題, 首先想的應該是,: 是否是我們試圖創建一個懸垂引用,或者是試圖匹配不一致的生命周期,而不是簡單粗暴的用 'static 來解決問題。
但是話說回來, 存在即合理,有時候, 'static 確實可以幫助我們解決非常復雜的生命周期問題, 甚至是無法被手動解決的生命周期問題, 那么此時就應該放心大膽的用, 只要你確定:? 你的所有引用的生命周期都是正確的。知識編譯器太笨不懂罷來。
總結下:
? ? ? ? 1.生命周期' static 意味著能和程序活得一樣久,例如字符串字面量和特征對象
? ? ? ? 2.是在遇到解決不了的生命周期標注問題, 可以嘗試 T: 'static ,有時候它會給你奇跡
一個復雜例子: 泛型 ,特征約束
手指已經疲軟物理,我好想停止,但是華麗的開場都要有與之匹配的謝幕,那我們就用一個稍微復雜點的例子來結束:?
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}
}
依然是熟悉的配方longest? , 但是多來一段廢話:ann ,因為要用格式化{}? 來輸出 ann , 因此需要實現 Display 特征。
總結
我不知道支撐我一口氣寫完的勇氣是什么, 也許是不做完不算夫斯基,也許是一些讀者對本書的期待,不管如何, 這章足足寫了17000字,可惜不是寫小說, 不然肯定可以獲取很多月票;
但是還沒完,是的,就算是將近兩萬字,生命周期的旅程依然沒有完結,在本書的進階部分,我們將介紹一些關于生命周期的高級特性, 這些特性你在其它中文書中目前還看不到的。