溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

rust生命周期源碼分析

發(fā)布時間:2023-03-17 11:22:12 來源:億速云 閱讀:99 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細介紹“rust生命周期源碼分析”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當(dāng),希望這篇“rust生命周期源碼分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

    rust生命周期

    生命周期是rust中用來規(guī)定引用的有效作用域。在大多數(shù)時候,無需手動聲明,因為編譯器能夠自動推導(dǎo)。當(dāng)編譯器無法自動推導(dǎo)出生命周期的時候,就需要我們手動標(biāo)明生命周期。生命周期主要是為了避免懸垂引用

    借用檢查

    rust的編譯器會使用借用檢查器來檢查我們程序的借用正確性。例如:

    #![allow(unused)]
    fn main() {
    {
        let r;
    
        {
            let x = 5;
            r = &x;
        }
    
        println!("r: {}", r);
    }
    }

    在編譯期,Rust 會比較兩個變量的生命周期,結(jié)果發(fā)現(xiàn) r 明明擁有生命周期 'a,但是卻引用了一個小得多的生命周期 'b,在這種情況下,編譯器會認為我們的程序存在風(fēng)險,因此拒絕運行。

    函數(shù)中的生命周期

    #![allow(unused)]
    fn main() {
    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    }

    執(zhí)行這段代碼,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`

    意思是函數(shù)返回類型是一個借用值,但是無法從函數(shù)的簽名中得知返回值是從x還是y借用的。并且給出了相應(yīng)的修復(fù)代碼。

    4 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
      |           ++++     ++          ++          ++

    按照這個提示,我們更改函數(shù)聲明。就會發(fā)現(xiàn)可以順利通過編譯。因此,像這樣的函數(shù),我們無法判斷它是返回x還是y,那么只好手動進行生命周期聲明。上面的提示就是手動聲明聲明周期的語法。

    手動聲明生命周期

    需要注意的是,標(biāo)記的生命周期只是為了取悅編譯器,讓編譯器不要難為我們,它不會改變?nèi)魏我玫膶嶋H作用域

    生命周期的語法是以&rsquo;開頭,名稱往往是一個單獨的小寫字母。大多數(shù)人用&rsquo;a來作為生命周期的名稱。如果是引用類型的參數(shù),生命周期會位于&之后,并用空格來將生命周期和參數(shù)分隔開。函數(shù)簽名中的生命周期標(biāo)注和泛型一樣,需要在提前聲明生命周期。例如我們剛才修改過的函數(shù)簽名

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

    該函數(shù)簽名表明對于某些生命周期 'a,函數(shù)的兩個參數(shù)都至少跟 'a 活得一樣久,同時函數(shù)的返回引用也至少跟 'a 活得一樣久。實際上,這意味著返回值的生命周期與參數(shù)生命周期中的較小值一致:雖然兩個參數(shù)的生命周期都是標(biāo)注了 'a,但是實際上這兩個參數(shù)的真實生命周期可能是不一樣的(生命周期 '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 的生命周期等于參數(shù)中生命周期最小的,因此要等于 string2 的生命周期,也就是說,result 要活得和 string2 一樣久。如過我們將上面的代碼改變?yōu)槿缦滤尽?/p>

    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);
    }

    那么將會導(dǎo)致錯誤,因為編譯器知道string2活不到最后一行打印。而string1可以活到打印,但是編譯器并不知道longest返回的是誰。
    函數(shù)的返回值如果是一個引用類型,那么它的生命周期只會來源于:

    • 函數(shù)參數(shù)的生命周期

    • 函數(shù)體中某個新建引用的生命周期

    若是后者情況,就是典型的懸垂引用場景:

    #![allow(unused)]
    fn main() {
    fn longest<'a>(x: &str, y: &str) -> &'a str {
        let result = String::from("really long string");
        result.as_str()
    }
    }

    上面的函數(shù)的返回值就和參數(shù) x,y 沒有任何關(guān)系,而是引用了函數(shù)體內(nèi)創(chuàng)建的字符串,而函數(shù)結(jié)束的時候會自動釋放result的內(nèi)存,從而導(dǎo)致懸垂指針。這種情況,最好的辦法就是返回內(nèi)部字符串的所有權(quán),然后把字符串的所有權(quán)轉(zhuǎn)移給調(diào)用者:

    fn longest<'a>(_x: &str, _y: &str) -> String {
        String::from("really long string")
    }
    
    fn main() {
       let s = longest("not", "important");
    }

    結(jié)構(gòu)體中的生命周期

    在結(jié)構(gòu)體中使用引用,只要為結(jié)構(gòu)體中的每一個引用標(biāo)注上生命周期即可。

    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函數(shù),因此這段代碼可以正常工作。
    ImportantExcerpt 結(jié)構(gòu)體中有一個引用類型的字段 part,因此需要為它標(biāo)注上生命周期。結(jié)構(gòu)體的生命周期標(biāo)注語法跟泛型參數(shù)語法很像,需要對生命周期參數(shù)進行聲明 <'a>。該生命周期標(biāo)注說明,結(jié)構(gòu)體 ImportantExcerpt 所引用的字符串 str 必須比該結(jié)構(gòu)體活得更久。

    生命周期消除

    編譯器為了簡化用戶的使用,運用了生命周期消除大法。例如:

    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 函數(shù),它的返回值是一個引用類型,那么該引用只有兩種情況:

    • 從參數(shù)獲取

    • 從函數(shù)體內(nèi)部新創(chuàng)建的變量獲取

    如果是后者,就會出現(xiàn)懸垂引用,最終被編譯器拒絕,因此只剩一種情況:返回值的引用是獲取自參數(shù),這就意味著參數(shù)和返回值的生命周期是一樣的。道理很簡單,我們能看出來,編譯器自然也能看出來,因此,就算我們不標(biāo)注生命周期,也不會產(chǎn)生歧義。

    只不過,消除規(guī)則不是萬能的,若編譯器不能確定某件事是正確時,會直接判為不正確,那么你還是需要手動標(biāo)注生命周期
    函數(shù)或者方法中,參數(shù)的生命周期被稱為 輸入生命周期,返回值的生命周期被稱為 輸出生命周期

    三條消除原則

    1.每一個引用參數(shù)都會獲得獨自的生命周期

    例如一個引用參數(shù)的函數(shù)就有一個生命周期標(biāo)注: fn foo<'a>(x: &'a i32),兩個引用參數(shù)的有兩個生命周期標(biāo)注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此類推。

    2.若只有一個輸入生命周期(函數(shù)參數(shù)中只有一個引用類型),那么該生命周期會被賦給所有的輸出生命周期,也就是所有返回值的生命周期都等于該輸入生命周期

    例如函數(shù) fn foo(x: &i32) -> &i32,x 參數(shù)的生命周期會被自動賦給返回值 &i32,因此該函數(shù)等同于 fn foo<'a>(x: &'a i32) -> &'a i32

    3.若存在多個輸入生命周期,且其中一個是 &self 或 &mut self,則 &self 的生命周期被賦給所有的輸出生命周期。

    擁有 &self 形式的參數(shù),說明該函數(shù)是一個 方法,該規(guī)則讓方法的使用便利度大幅提升。

    讓我們假裝自己是編譯器,然后看下以下的函數(shù)該如何應(yīng)用這些規(guī)則:

    例子1

    fn first_word(s: &str) -> &str // 實際項目中的手寫代碼

    首先,我們手寫的代碼如上所示時,編譯器會先應(yīng)用第一條規(guī)則,為每個參數(shù)標(biāo)注一個生命周期:

    fn first_word<'a>(s: &'a str) -> &str  // 編譯器自動為參數(shù)添加生命周期

    此時,第二條規(guī)則就可以進行應(yīng)用,因為函數(shù)只有一個輸入生命周期,因此該生命周期會被賦予所有的輸出生命周期:

    fn first_word<'a>(s: &'a str) -> &'a str  // 編譯器自動為返回值添加生命周期

    此時,編譯器為函數(shù)簽名中的所有引用都自動添加了具體的生命周期,因此編譯通過,且用戶無需手動去標(biāo)注生命周期,只要按照 fn first_word(s: &str) -> &str的形式寫代碼即可。

    例子2

    fn longest(x: &str, y: &str) -> &str // 實際項目中的手寫代碼

    首先,編譯器會應(yīng)用第一條規(guī)則,為每個參數(shù)都標(biāo)注生命周期:

    fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str

    但是此時,第二條規(guī)則卻無法被使用,因為輸入生命周期有兩個,第三條規(guī)則也不符合,因為它是函數(shù),不是方法,因此沒有 &self 參數(shù)。在套用所有規(guī)則后,編譯器依然無法為返回值標(biāo)注合適的生命周期,因此,編譯器就會報錯,提示我們需要手動標(biāo)注生命周期。

    例子3

    impl<'a> ImportantExcerpt<'a> {
        fn announce_and_return_part(&self, announcement: &str) -> &str {
            println!("Attention please: {}", announcement);
            self.part
        }
    }

    首先,編譯器應(yīng)用第一規(guī)則,給予每個輸入?yún)?shù)一個生命周期。

    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。接著,編譯器應(yīng)用第三規(guī)則,將 &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
        }
    }

    盡管我們沒有給方法標(biāo)注生命周期,但是在第一和第三規(guī)則的配合下,編譯器依然完美的為我們亮起了綠燈。

    生命周期約束

    我們來看下面這個例子。將返回值的生命周期聲明為&rsquo;b,但是實際返回的是生命周期為&rsquo;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 約束生命周期關(guān)系,如下:

    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
        }
    }

    加上這個約束,告訴編譯器&rsquo;a活的比&rsquo;b更久,引用&rsquo;a不會產(chǎn)生懸垂指針(無效引用)。

    靜態(tài)生命周期

    rust中有一個非常特殊的生命周期,那就是&rsquo;static,擁有該生命周期的引用可以活的和整個程序一樣久。實際上字符串字面值就擁有&rsquo;static生命周期,它被硬編碼進rust的二進制文件中。'static生命周期非常強大,隨意使用它相當(dāng)于放棄了生命周期檢查。遇到因為生命周期導(dǎo)致的編譯不通過問題,首先想的應(yīng)該是:是否是我們試圖創(chuàng)建一個懸垂引用,或者是試圖匹配不一致的生命周期,而不是簡單粗暴的用 'static 來解決問題。除非實在遇到解決不了的生命周期標(biāo)注問題,可以嘗試&rsquo;static生命周期。例如:

    fn t() -> &'static str{
        "qwert"
    }
    
    fn t() -> &'static str{
        "qwert"
    }

    注意,使用&rsquo;static生命周期的時候,不需要提前聲明。

    一個復(fù)雜例子: 泛型,特征約束以及生命周期

    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
        }
    }

    例子中,包含了生命周期&rsquo;a,泛型T以及對T的約束Display(因為我們需要打印ann)。

    讀到這里,這篇“rust生命周期源碼分析”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI