您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“rust生命周期源碼分析”,內容詳細,步驟清晰,細節處理妥當,希望這篇“rust生命周期源碼分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
生命周期是rust中用來規定引用的有效作用域。在大多數時候,無需手動聲明,因為編譯器能夠自動推導。當編譯器無法自動推導出生命周期的時候,就需要我們手動標明生命周期。生命周期主要是為了避免懸垂引用。
rust的編譯器會使用借用檢查器來檢查我們程序的借用正確性。例如:
#![allow(unused)] fn main() { { let r; { let x = 5; r = &x; } println!("r: {}", r); } }
在編譯期,Rust 會比較兩個變量的生命周期,結果發現 r 明明擁有生命周期 'a,但是卻引用了一個小得多的生命周期 'b,在這種情況下,編譯器會認為我們的程序存在風險,因此拒絕運行。
#![allow(unused)] fn main() { fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } }
執行這段代碼,rust編譯器會報錯,它給出的help信息如下:
help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
意思是函數返回類型是一個借用值,但是無法從函數的簽名中得知返回值是從x還是y借用的。并且給出了相應的修復代碼。
4 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str | ++++ ++ ++ ++
按照這個提示,我們更改函數聲明。就會發現可以順利通過編譯。因此,像這樣的函數,我們無法判斷它是返回x還是y,那么只好手動進行生命周期聲明。上面的提示就是手動聲明聲明周期的語法。
需要注意的是,標記的生命周期只是為了取悅編譯器,讓編譯器不要難為我們,它不會改變任何引用的實際作用域。
生命周期的語法是以’開頭,名稱往往是一個單獨的小寫字母。大多數人用’a來作為生命周期的名稱。如果是引用類型的參數,生命周期會位于&之后,并用空格來將生命周期和參數分隔開。函數簽名中的生命周期標注和泛型一樣,需要在提前聲明生命周期。例如我們剛才修改過的函數簽名
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
該函數簽名表明對于某些生命周期 'a,函數的兩個參數都至少跟 'a 活得一樣久,同時函數的返回引用也至少跟 'a 活得一樣久。實際上,這意味著返回值的生命周期與參數生命周期中的較小值一致:雖然兩個參數的生命周期都是標注了 'a,但是實際上這兩個參數的真實生命周期可能是不一樣的(生命周期 'a 不代表生命周期等于 'a,而是大于等于 'a)。例如:
fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {}", result); } }
result 的生命周期等于參數中生命周期最小的,因此要等于 string2 的生命周期,也就是說,result 要活得和 string2 一樣久。如過我們將上面的代碼改變為如下所示。
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); }
那么將會導致錯誤,因為編譯器知道string2活不到最后一行打印。而string1可以活到打印,但是編譯器并不知道longest返回的是誰。
函數的返回值如果是一個引用類型,那么它的生命周期只會來源于:
函數參數的生命周期
函數體中某個新建引用的生命周期
若是后者情況,就是典型的懸垂引用場景:
#![allow(unused)] fn main() { fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); result.as_str() } }
上面的函數的返回值就和參數 x,y 沒有任何關系,而是引用了函數體內創建的字符串,而函數結束的時候會自動釋放result的內存,從而導致懸垂指針。這種情況,最好的辦法就是返回內部字符串的所有權,然后把字符串的所有權轉移給調用者:
fn longest<'a>(_x: &str, _y: &str) -> String { String::from("really long string") } fn main() { let s = longest("not", "important"); }
在結構體中使用引用,只要為結構體中的每一個引用標注上生命周期即可。
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, }; }
part引用的first_sentence來自于novel,它的生命周期是main函數,因此這段代碼可以正常工作。
ImportantExcerpt 結構體中有一個引用類型的字段 part,因此需要為它標注上生命周期。結構體的生命周期標注語法跟泛型參數語法很像,需要對生命周期參數進行聲明 <'a>。該生命周期標注說明,結構體 ImportantExcerpt 所引用的字符串 str 必須比該結構體活得更久。
編譯器為了簡化用戶的使用,運用了生命周期消除大法。例如:
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.每一個引用參數都會獲得獨自的生命周期
例如一個引用參數的函數就有一個生命周期標注: 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 形式的參數,說明該函數是一個 方法,該規則讓方法的使用便利度大幅提升。
讓我們假裝自己是編譯器,然后看下以下的函數該如何應用這些規則:
例子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 參數。在套用所有規則后,編譯器依然無法為返回值標注合適的生命周期,因此,編譯器就會報錯,提示我們需要手動標注生命周期。
例子3
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_part<'b>(&'a self, announcement: &'b str) -> &'a str { println!("Attention please: {}", announcement); self.part } }
盡管我們沒有給方法標注生命周期,但是在第一和第三規則的配合下,編譯器依然完美的為我們亮起了綠燈。
我們來看下面這個例子。將返回值的生命周期聲明為’b,但是實際返回的是生命周期為’a的self.part。
impl<'a: 'b, 'b> ImportantExcerpt<'a> { fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str { println!("Attention please: {}", announcement); self.part } }
'a: 'b,是生命周期約束語法,跟泛型約束非常相似,用于說明 'a 必須比 'b 活得久
可以把 'a 和 'b 都在同一個地方聲明(如上),或者分開聲明但通過 where 'a: 'b 約束生命周期關系,如下:
impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str where 'a: 'b, { println!("Attention please: {}", announcement); self.part } }
加上這個約束,告訴編譯器’a活的比’b更久,引用’a不會產生懸垂指針(無效引用)。
rust中有一個非常特殊的生命周期,那就是’static,擁有該生命周期的引用可以活的和整個程序一樣久。實際上字符串字面值就擁有’static生命周期,它被硬編碼進rust的二進制文件中。'static生命周期非常強大,隨意使用它相當于放棄了生命周期檢查。遇到因為生命周期導致的編譯不通過問題,首先想的應該是:是否是我們試圖創建一個懸垂引用,或者是試圖匹配不一致的生命周期,而不是簡單粗暴的用 'static 來解決問題。除非實在遇到解決不了的生命周期標注問題,可以嘗試’static生命周期。例如:
fn t() -> &'static str{ "qwert" } fn t() -> &'static str{ "qwert" }
注意,使用’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 } }
例子中,包含了生命周期’a,泛型T以及對T的約束Display(因為我們需要打印ann)。
讀到這里,這篇“rust生命周期源碼分析”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。