99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Rust 生命周期確保引用有效

2023-03-22 15:10 更新
ch10-03-lifetime-syntax.md
commit 1b8746013079f2e2ce1c8e85f633d9769778ea7f

當(dāng)在第四章討論 “引用和借用” 部分時(shí),我們遺漏了一個(gè)重要的細(xì)節(jié):Rust 中的每一個(gè)引用都有其 生命周期lifetime),也就是引用保持有效的作用域。大部分時(shí)候生命周期是隱含并可以推斷的,正如大部分時(shí)候類型也是可以推斷的一樣。類似于當(dāng)因?yàn)橛卸喾N可能類型的時(shí)候必須注明類型,也會(huì)出現(xiàn)引用的生命周期以一些不同方式相關(guān)聯(lián)的情況,所以 Rust 需要我們使用泛型生命周期參數(shù)來注明他們的關(guān)系,這樣就能確保運(yùn)行時(shí)實(shí)際使用的引用絕對(duì)是有效的。

生命周期注解甚至不是一個(gè)大部分語(yǔ)言都有的概念,所以這可能感覺起來有些陌生。雖然本章不可能涉及到它全部的內(nèi)容,我們會(huì)講到一些通常你可能會(huì)遇到的生命周期語(yǔ)法以便你熟悉這個(gè)概念。

生命周期避免了懸垂引用

生命周期的主要目標(biāo)是避免懸垂引用,后者會(huì)導(dǎo)致程序引用了非預(yù)期引用的數(shù)據(jù)。考慮一下示例 10-17 中的程序,它有一個(gè)外部作用域和一個(gè)內(nèi)部作用域。

    {
        let r;

        {
            let x = 5;
            r = &x;
        }

        println!("r: {}", r);
    }

示例 10-17:嘗試使用離開作用域的值的引用

注意:示例 10-17、10-18 和 10-24 中聲明了沒有初始值的變量,所以這些變量存在于外部作用域。這乍看之下好像和 Rust 不允許存在空值相沖突。然而如果嘗試在給它一個(gè)值之前使用這個(gè)變量,會(huì)出現(xiàn)一個(gè)編譯時(shí)錯(cuò)誤,這就說明了 Rust 確實(shí)不允許空值。

外部作用域聲明了一個(gè)沒有初值的變量 r,而內(nèi)部作用域聲明了一個(gè)初值為 5 的變量x。在內(nèi)部作用域中,我們嘗試將 r 的值設(shè)置為一個(gè) x 的引用。接著在內(nèi)部作用域結(jié)束后,嘗試打印出 r 的值。這段代碼不能編譯因?yàn)?nbsp;r 引用的值在嘗試使用之前就離開了作用域。如下是錯(cuò)誤信息:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
  --> src/main.rs:7:17
   |
7  |             r = &x;
   |                 ^^ borrowed value does not live long enough
8  |         }
   |         - `x` dropped here while still borrowed
9  | 
10 |         println!("r: {}", r);
   |                           - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

變量 x 并沒有 “存在的足夠久”。其原因是 x 在到達(dá)第 7 行內(nèi)部作用域結(jié)束時(shí)就離開了作用域。不過 r 在外部作用域仍是有效的;作用域越大我們就說它 “存在的越久”。如果 Rust 允許這段代碼工作,r 將會(huì)引用在 x 離開作用域時(shí)被釋放的內(nèi)存,這時(shí)嘗試對(duì) r 做任何操作都不能正常工作。那么 Rust 是如何決定這段代碼是不被允許的呢?這得益于借用檢查器。

借用檢查器

Rust 編譯器有一個(gè) 借用檢查器borrow checker),它比較作用域來確保所有的借用都是有效的。示例 10-18 展示了與示例 10-17 相同的例子不過帶有變量生命周期的注釋:

    {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         // ---------+

示例 10-18:r 和 x 的生命周期注解,分別叫做 'a 和 'b

這里將 r 的生命周期標(biāo)記為 'a 并將 x 的生命周期標(biāo)記為 'b。如你所見,內(nèi)部的 'b 塊要比外部的生命周期 'a 小得多。在編譯時(shí),Rust 比較這兩個(gè)生命周期的大小,并發(fā)現(xiàn) r 擁有生命周期 'a,不過它引用了一個(gè)擁有生命周期 'b 的對(duì)象。程序被拒絕編譯,因?yàn)樯芷?nbsp;'b 比生命周期 'a 要?。罕灰玫膶?duì)象比它的引用者存在的時(shí)間更短。

讓我們看看示例 10-19 中這個(gè)并沒有產(chǎn)生懸垂引用且可以正確編譯的例子:

    {
        let x = 5;            // ----------+-- 'b
                              //           |
        let r = &x;           // --+-- 'a  |
                              //   |       |
        println!("r: {}", r); //   |       |
                              // --+       |
    }                         // ----------+

示例 10-19:一個(gè)有效的引用,因?yàn)閿?shù)據(jù)比引用有著更長(zhǎng)的生命周期

這里 x 擁有生命周期 'b,比 'a 要大。這就意味著 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的時(shí)候也總是有效的。

現(xiàn)在我們已經(jīng)在一個(gè)具體的例子中展示了引用的生命周期位于何處,并討論了 Rust 如何分析生命周期來保證引用總是有效的,接下來讓我們聊聊在函數(shù)的上下文中參數(shù)和返回值的泛型生命周期。

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

讓我們來編寫一個(gè)返回兩個(gè)字符串 slice 中較長(zhǎng)者的函數(shù)。這個(gè)函數(shù)獲取兩個(gè)字符串 slice 并返回一個(gè)字符串 slice。一旦我們實(shí)現(xiàn)了 longest 函數(shù),示例 10-20 中的代碼應(yīng)該會(huì)打印出 The longest string is abcd

文件名: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

示例 10-20:main 函數(shù)調(diào)用 longest 函數(shù)來尋找兩個(gè)字符串 slice 中較長(zhǎng)的一個(gè)

注意這個(gè)函數(shù)獲取作為引用的字符串 slice,因?yàn)槲覀儾幌M?nbsp;longest 函數(shù)獲取參數(shù)的所有權(quán)。參考之前第四章中的 “字符串 slice 作為參數(shù)” 部分中更多關(guān)于為什么示例 10-20 的參數(shù)正符合我們期望的討論。

如果嘗試像示例 10-21 中那樣實(shí)現(xiàn) longest 函數(shù),它并不能編譯:

文件名: src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

示例 10-21:一個(gè) longest 函數(shù)的實(shí)現(xiàn),它返回兩個(gè)字符串 slice 中較長(zhǎng)者,現(xiàn)在還不能編譯

相應(yīng)地會(huì)出現(xiàn)如下有關(guān)生命周期的錯(cuò)誤:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &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`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

提示文本揭示了返回值需要一個(gè)泛型生命周期參數(shù),因?yàn)?Rust 并不知道將要返回的引用是指向 x 或 y。事實(shí)上我們也不知道,因?yàn)楹瘮?shù)體中 if 塊返回一個(gè) x 的引用而 else 塊返回一個(gè) y 的引用!

當(dāng)我們定義這個(gè)函數(shù)的時(shí)候,并不知道傳遞給函數(shù)的具體值,所以也不知道到底是 if 還是 else 會(huì)被執(zhí)行。我們也不知道傳入的引用的具體生命周期,所以也就不能像示例 10-18 和 10-19 那樣通過觀察作用域來確定返回的引用是否總是有效。借用檢查器自身同樣也無法確定,因?yàn)樗恢?nbsp;x 和 y 的生命周期是如何與返回值的生命周期相關(guān)聯(lián)的。為了修復(fù)這個(gè)錯(cuò)誤,我們將增加泛型生命周期參數(shù)來定義引用間的關(guān)系以便借用檢查器可以進(jìn)行分析。

生命周期注解語(yǔ)法

生命周期注解并不改變?nèi)魏我玫纳芷诘拈L(zhǎng)短。與當(dāng)函數(shù)簽名中指定了泛型類型參數(shù)后就可以接受任何類型一樣,當(dāng)指定了泛型生命周期后函數(shù)也能接受任何生命周期的引用。生命周期注解描述了多個(gè)引用生命周期相互的關(guān)系,而不影響其生命周期。

生命周期注解有著一個(gè)不太常見的語(yǔ)法:生命周期參數(shù)名稱必須以撇號(hào)(')開頭,其名稱通常全是小寫,類似于泛型其名稱非常短。'a 是大多數(shù)人默認(rèn)使用的名稱。生命周期參數(shù)注解位于引用的 & 之后,并有一個(gè)空格來將引用類型與生命周期注解分隔開。

這里有一些例子:我們有一個(gè)沒有生命周期參數(shù)的 i32 的引用,一個(gè)有叫做 'a 的生命周期參數(shù)的 i32 的引用,和一個(gè)生命周期也是 'a 的 i32 的可變引用:

&i32        // 引用
&'a i32     // 帶有顯式生命周期的引用
&'a mut i32 // 帶有顯式生命周期的可變引用

單個(gè)的生命周期注解本身沒有多少意義,因?yàn)樯芷谧⒔飧嬖V Rust 多個(gè)引用的泛型生命周期參數(shù)如何相互聯(lián)系的。例如如果函數(shù)有一個(gè)生命周期 'a 的 i32 的引用的參數(shù) first。還有另一個(gè)同樣是生命周期 'a 的 i32 的引用的參數(shù) second。這兩個(gè)生命周期注解意味著引用 first 和 second 必須與這泛型生命周期存在得一樣久。

函數(shù)簽名中的生命周期注解

現(xiàn)在來看看 longest 函數(shù)的上下文中的生命周期。就像泛型類型參數(shù),泛型生命周期參數(shù)需要聲明在函數(shù)名和參數(shù)列表間的尖括號(hào)中。 在這個(gè)簽名中我們想要表達(dá)的限制是所有(兩個(gè))參數(shù)和返回的引用的生命周期是相關(guān)的,也就是這兩個(gè)參數(shù)和返回的引用存活的一樣久。就像示例 10-22 中在每個(gè)引用中都加上了 'a 那樣:

文件名: src/main.rs

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

示例 10-22:longest 函數(shù)定義指定了簽名中所有的引用必須有相同的生命周期 'a

這段代碼能夠編譯并會(huì)產(chǎn)生我們希望得到的示例 10-20 中的 main 函數(shù)的結(jié)果。

現(xiàn)在函數(shù)簽名表明對(duì)于某些生命周期 'a,函數(shù)會(huì)獲取兩個(gè)參數(shù),他們都是與生命周期 'a 存在的一樣長(zhǎng)的字符串 slice。函數(shù)會(huì)返回一個(gè)同樣也與生命周期 'a 存在的一樣長(zhǎng)的字符串 slice。它的實(shí)際含義是 longest 函數(shù)返回的引用的生命周期與傳入該函數(shù)的引用的生命周期的較小者一致。這些關(guān)系就是我們希望 Rust 分析代碼時(shí)所使用的。

記住通過在函數(shù)簽名中指定生命周期參數(shù)時(shí),我們并沒有改變?nèi)魏蝹魅胫祷蚍祷刂档纳芷冢侵赋鋈魏尾粷M足這個(gè)約束條件的值都將被借用檢查器拒絕。注意 longest 函數(shù)并不需要知道 x 和 y 具體會(huì)存在多久,而只需要知道有某個(gè)可以被 'a 替代的作用域?qū)?huì)滿足這個(gè)簽名。

當(dāng)在函數(shù)中使用生命周期注解時(shí),這些注解出現(xiàn)在函數(shù)簽名中,而不存在于函數(shù)體中的任何代碼中。生命周期注解成為了函數(shù)約定的一部分,非常像簽名中的類型。讓函數(shù)簽名包含生命周期約定意味著 Rust 編譯器的工作變得更簡(jiǎn)單了。如果函數(shù)注解有誤或者調(diào)用方法不對(duì),編譯器錯(cuò)誤可以更準(zhǔn)確地指出代碼和限制的部分。如果不這么做的話,Rust 編譯會(huì)對(duì)我們期望的生命周期關(guān)系做更多的推斷,這樣編譯器可能只能指出離出問題地方很多步之外的代碼。

當(dāng)具體的引用被傳遞給 longest 時(shí),被 'a 所替代的具體生命周期是 x 的作用域與 y 的作用域相重疊的那一部分。換一種說法就是泛型生命周期 'a 的具體生命周期等同于 x 和 y 的生命周期中較小的那一個(gè)。因?yàn)槲覀冇孟嗤纳芷趨?shù) 'a 標(biāo)注了返回的引用值,所以返回的引用值就能保證在 x 和 y 中較短的那個(gè)生命周期結(jié)束之前保持有效。

讓我們看看如何通過傳遞擁有不同具體生命周期的引用來限制 longest 函數(shù)的使用。示例 10-23 是一個(gè)很直觀的例子。

文件名: src/main.rs

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

示例 10-23:通過擁有不同的具體生命周期的 String 值調(diào)用 longest 函數(shù)

在這個(gè)例子中,string1 直到外部作用域結(jié)束都是有效的,string2 則在內(nèi)部作用域中是有效的,而 result 則引用了一些直到內(nèi)部作用域結(jié)束都是有效的值。借用檢查器認(rèn)可這些代碼;它能夠編譯和運(yùn)行,并打印出 The longest string is long string is long。

接下來,讓我們嘗試另外一個(gè)例子,該例子揭示了 result 的引用的生命周期必須是兩個(gè)參數(shù)中較短的那個(gè)。以下代碼將 result 變量的聲明移動(dòng)出內(nèi)部作用域,但是將 result 和 string2 變量的賦值語(yǔ)句一同留在內(nèi)部作用域中。接著,使用了變量 result 的 println! 也被移動(dòng)到內(nèi)部作用域之外。注意示例 10-24 中的代碼不能通過編譯:

文件名: src/main.rs

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

示例 10-24:嘗試在 string2 離開作用域之后使用 result

如果嘗試編譯會(huì)出現(xiàn)如下錯(cuò)誤:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

錯(cuò)誤表明為了保證 println! 中的 result 是有效的,string2 需要直到外部作用域結(jié)束都是有效的。Rust 知道這些是因?yàn)椋?code>longest)函數(shù)的參數(shù)和返回值都使用了相同的生命周期參數(shù) 'a

如果從人的角度讀上述代碼,我們可能會(huì)覺得這個(gè)代碼是正確的。 string1 更長(zhǎng),因此 result 會(huì)包含指向 string1 的引用。因?yàn)?nbsp;string1 尚未離開作用域,對(duì)于 println! 來說 string1 的引用仍然是有效的。然而,我們通過生命周期參數(shù)告訴 Rust 的是: longest 函數(shù)返回的引用的生命周期應(yīng)該與傳入?yún)?shù)的生命周期中較短那個(gè)保持一致。因此,借用檢查器不允許示例 10-24 中的代碼,因?yàn)樗赡軙?huì)存在無效的引用。

請(qǐng)嘗試更多采用不同的值和不同生命周期的引用作為 longest 函數(shù)的參數(shù)和返回值的實(shí)驗(yàn)。并在開始編譯前猜想你的實(shí)驗(yàn)?zāi)芊裢ㄟ^借用檢查器,接著編譯一下看看你的理解是否正確!

深入理解生命周期

指定生命周期參數(shù)的正確方式依賴函數(shù)實(shí)現(xiàn)的具體功能。例如,如果將 longest 函數(shù)的實(shí)現(xiàn)修改為總是返回第一個(gè)參數(shù)而不是最長(zhǎng)的字符串 slice,就不需要為參數(shù) y 指定一個(gè)生命周期。如下代碼將能夠編譯:

文件名: src/main.rs

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

在這個(gè)例子中,我們?yōu)閰?shù) x 和返回值指定了生命周期參數(shù) 'a,不過沒有為參數(shù) y 指定,因?yàn)?nbsp;y 的生命周期與參數(shù) x 和返回值的生命周期沒有任何關(guān)系。

當(dāng)從函數(shù)返回一個(gè)引用,返回值的生命周期參數(shù)需要與一個(gè)參數(shù)的生命周期參數(shù)相匹配。如果返回的引用 沒有 指向任何一個(gè)參數(shù),那么唯一的可能就是它指向一個(gè)函數(shù)內(nèi)部創(chuàng)建的值,它將會(huì)是一個(gè)懸垂引用,因?yàn)樗鼘?huì)在函數(shù)結(jié)束時(shí)離開作用域。嘗試考慮這個(gè)并不能編譯的 longest 函數(shù)實(shí)現(xiàn):

文件名: src/main.rs

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

即便我們?yōu)榉祷刂抵付松芷趨?shù) 'a,這個(gè)實(shí)現(xiàn)卻編譯失敗了,因?yàn)榉祷刂档纳芷谂c參數(shù)完全沒有關(guān)聯(lián)。這里是會(huì)出現(xiàn)的錯(cuò)誤信息:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error

出現(xiàn)的問題是 result 在 longest 函數(shù)的結(jié)尾將離開作用域并被清理,而我們嘗試從函數(shù)返回一個(gè) result 的引用。無法指定生命周期參數(shù)來改變懸垂引用,而且 Rust 也不允許我們創(chuàng)建一個(gè)懸垂引用。在這種情況,最好的解決方案是返回一個(gè)有所有權(quán)的數(shù)據(jù)類型而不是一個(gè)引用,這樣函數(shù)調(diào)用者就需要負(fù)責(zé)清理這個(gè)值了。

綜上,生命周期語(yǔ)法是用于將函數(shù)的多個(gè)參數(shù)與其返回值的生命周期進(jìn)行關(guān)聯(lián)的。一旦他們形成了某種關(guān)聯(lián),Rust 就有了足夠的信息來允許內(nèi)存安全的操作并阻止會(huì)產(chǎn)生懸垂指針亦或是違反內(nèi)存安全的行為。

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

目前為止,我們只定義過有所有權(quán)類型的結(jié)構(gòu)體。接下來,我們將定義包含引用的結(jié)構(gòu)體,不過這需要為結(jié)構(gòu)體定義中的每一個(gè)引用添加生命周期注解。示例 10-25 中有一個(gè)存放了一個(gè)字符串 slice 的結(jié)構(gòu)體 ImportantExcerpt

文件名: src/main.rs

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

示例 10-25:一個(gè)存放引用的結(jié)構(gòu)體,所以其定義需要生命周期注解

這個(gè)結(jié)構(gòu)體有一個(gè)字段,part,它存放了一個(gè)字符串 slice,這是一個(gè)引用。類似于泛型參數(shù)類型,必須在結(jié)構(gòu)體名稱后面的尖括號(hào)中聲明泛型生命周期參數(shù),以便在結(jié)構(gòu)體定義中使用生命周期參數(shù)。這個(gè)注解意味著 ImportantExcerpt 的實(shí)例不能比其 part 字段中的引用存在的更久。

這里的 main 函數(shù)創(chuàng)建了一個(gè) ImportantExcerpt 的實(shí)例,它存放了變量 novel 所擁有的 String 的第一個(gè)句子的引用。novel 的數(shù)據(jù)在 ImportantExcerpt 實(shí)例創(chuàng)建之前就存在。另外,直到 ImportantExcerpt 離開作用域之后 novel 都不會(huì)離開作用域,所以 ImportantExcerpt 實(shí)例中的引用是有效的。

生命周期省略(Lifetime Elision)

現(xiàn)在我們已經(jīng)知道了每一個(gè)引用都有一個(gè)生命周期,而且我們需要為那些使用了引用的函數(shù)或結(jié)構(gòu)體指定生命周期。然而,第四章的示例 4-9 中有一個(gè)函數(shù),如示例 10-26 所示,它沒有生命周期注解卻能編譯成功:

文件名: src/lib.rs

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[..]
}

示例 10-26:示例 4-9 定義了一個(gè)沒有使用生命周期注解的函數(shù),即便其參數(shù)和返回值都是引用

這個(gè)函數(shù)沒有生命周期注解卻能編譯是由于一些歷史原因:在早期版本(pre-1.0)的 Rust 中,這的確是不能編譯的。每一個(gè)引用都必須有明確的生命周期。那時(shí)的函數(shù)簽名將會(huì)寫成這樣:

fn first_word<'a>(s: &'a str) -> &'a str {

在編寫了很多 Rust 代碼后,Rust 團(tuán)隊(duì)發(fā)現(xiàn)在特定情況下 Rust 程序員們總是重復(fù)地編寫一模一樣的生命周期注解。這些場(chǎng)景是可預(yù)測(cè)的并且遵循幾個(gè)明確的模式。接著 Rust 團(tuán)隊(duì)就把這些模式編碼進(jìn)了 Rust 編譯器中,如此借用檢查器在這些情況下就能推斷出生命周期而不再?gòu)?qiáng)制程序員顯式的增加注解。

這里我們提到一些 Rust 的歷史是因?yàn)楦嗟拿鞔_的模式被合并和添加到編譯器中是完全可能的。未來只會(huì)需要更少的生命周期注解。

被編碼進(jìn) Rust 引用分析的模式被稱為 生命周期省略規(guī)則lifetime elision rules)。這并不是需要程序員遵守的規(guī)則;這些規(guī)則是一系列特定的場(chǎng)景,此時(shí)編譯器會(huì)考慮,如果代碼符合這些場(chǎng)景,就無需明確指定生命周期。

省略規(guī)則并不提供完整的推斷:如果 Rust 在明確遵守這些規(guī)則的前提下變量的生命周期仍然是模棱兩可的話,它不會(huì)猜測(cè)剩余引用的生命周期應(yīng)該是什么。在這種情況,編譯器會(huì)給出一個(gè)錯(cuò)誤,這可以通過增加對(duì)應(yīng)引用之間相聯(lián)系的生命周期注解來解決。

函數(shù)或方法的參數(shù)的生命周期被稱為 輸入生命周期input lifetimes),而返回值的生命周期被稱為 輸出生命周期output lifetimes)。

編譯器采用三條規(guī)則來判斷引用何時(shí)不需要明確的注解。第一條規(guī)則適用于輸入生命周期,后兩條規(guī)則適用于輸出生命周期。如果編譯器檢查完這三條規(guī)則后仍然存在沒有計(jì)算出生命周期的引用,編譯器將會(huì)停止并生成錯(cuò)誤。這些規(guī)則適用于 fn 定義,以及 impl 塊。

第一條規(guī)則是每一個(gè)是引用的參數(shù)都有它自己的生命周期參數(shù)。換句話說就是,有一個(gè)引用參數(shù)的函數(shù)有一個(gè)生命周期參數(shù):fn foo<'a>(x: &'a i32),有兩個(gè)引用參數(shù)的函數(shù)有兩個(gè)不同的生命周期參數(shù),fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此類推。

第二條規(guī)則是如果只有一個(gè)輸入生命周期參數(shù),那么它被賦予所有輸出生命周期參數(shù):fn foo<'a>(x: &'a i32) -> &'a i32。

第三條規(guī)則是如果方法有多個(gè)輸入生命周期參數(shù)并且其中一個(gè)參數(shù)是 &self 或 &mut self,說明是個(gè)對(duì)象的方法(method)(譯者注: 這里涉及rust的面向?qū)ο髤⒁?7章),那么所有輸出生命周期參數(shù)被賦予 self 的生命周期。第三條規(guī)則使得方法更容易讀寫,因?yàn)橹恍韪俚姆?hào)。

假設(shè)我們自己就是編譯器。并應(yīng)用這些規(guī)則來計(jì)算示例 10-26 中 first_word 函數(shù)簽名中的引用的生命周期。開始時(shí)簽名中的引用并沒有關(guān)聯(lián)任何生命周期:

fn first_word(s: &str) -> &str {

接著編譯器應(yīng)用第一條規(guī)則,也就是每個(gè)引用參數(shù)都有其自己的生命周期。我們像往常一樣稱之為 'a,所以現(xiàn)在簽名看起來像這樣:

fn first_word<'a>(s: &'a str) -> &str {

對(duì)于第二條規(guī)則,因?yàn)檫@里正好只有一個(gè)輸入生命周期參數(shù)所以是適用的。第二條規(guī)則表明輸入?yún)?shù)的生命周期將被賦予輸出生命周期參數(shù),所以現(xiàn)在簽名看起來像這樣:

fn first_word<'a>(s: &'a str) -> &'a str {

現(xiàn)在這個(gè)函數(shù)簽名中的所有引用都有了生命周期,如此編譯器可以繼續(xù)它的分析而無須程序員標(biāo)記這個(gè)函數(shù)簽名中的生命周期。

讓我們?cè)倏纯戳硪粋€(gè)例子,這次我們從示例 10-21 中沒有生命周期參數(shù)的 longest 函數(shù)開始:

fn longest(x: &str, y: &str) -> &str {

再次假設(shè)我們自己就是編譯器并應(yīng)用第一條規(guī)則:每個(gè)引用參數(shù)都有其自己的生命周期。這次有兩個(gè)參數(shù),所以就有兩個(gè)(不同的)生命周期:

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

再來應(yīng)用第二條規(guī)則,因?yàn)楹瘮?shù)存在多個(gè)輸入生命周期,它并不適用于這種情況。再來看第三條規(guī)則,它同樣也不適用,這是因?yàn)闆]有 self 參數(shù)。應(yīng)用了三個(gè)規(guī)則之后編譯器還沒有計(jì)算出返回值類型的生命周期。這就是為什么在編譯示例 10-21 的代碼時(shí)會(huì)出現(xiàn)錯(cuò)誤的原因:編譯器使用所有已知的生命周期省略規(guī)則,仍不能計(jì)算出簽名中所有引用的生命周期。

因?yàn)榈谌龡l規(guī)則真正能夠適用的就只有方法簽名,現(xiàn)在就讓我們看看那種情況中的生命周期,并看看為什么這條規(guī)則意味著我們經(jīng)常不需要在方法簽名中標(biāo)注生命周期。

方法定義中的生命周期注解

當(dāng)為帶有生命周期的結(jié)構(gòu)體實(shí)現(xiàn)方法時(shí),其語(yǔ)法依然類似示例 10-11 中展示的泛型類型參數(shù)的語(yǔ)法。聲明和使用生命周期參數(shù)的位置依賴于生命周期參數(shù)是否同結(jié)構(gòu)體字段或方法參數(shù)和返回值相關(guān)。

(實(shí)現(xiàn)方法時(shí))結(jié)構(gòu)體字段的生命周期必須總是在 impl 關(guān)鍵字之后聲明并在結(jié)構(gòu)體名稱之后被使用,因?yàn)檫@些生命周期是結(jié)構(gòu)體類型的一部分。

impl 塊里的方法簽名中,引用可能與結(jié)構(gòu)體字段中的引用相關(guān)聯(lián),也可能是獨(dú)立的。另外,生命周期省略規(guī)則也經(jīng)常讓我們無需在方法簽名中使用生命周期注解。讓我們看看一些使用示例 10-25 中定義的結(jié)構(gòu)體 ImportantExcerpt 的例子。

首先,這里有一個(gè)方法 level。其唯一的參數(shù)是 self 的引用,而且返回值只是一個(gè) i32,并不引用任何值:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl 之后和類型名稱之后的生命周期參數(shù)是必要的,不過因?yàn)榈谝粭l生命周期規(guī)則我們并不必須標(biāo)注 self 引用的生命周期。

這里是一個(gè)適用于第三條生命周期省略規(guī)則的例子:

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

這里有兩個(gè)輸入生命周期,所以 Rust 應(yīng)用第一條生命周期省略規(guī)則并給予 &self 和 announcement 他們各自的生命周期。接著,因?yàn)槠渲幸粋€(gè)參數(shù)是 &self,返回值類型被賦予了 &self 的生命周期,這樣所有的生命周期都被計(jì)算出來了。

靜態(tài)生命周期

這里有一種特殊的生命周期值得討論:'static,其生命周期能夠存活于整個(gè)程序期間。所有的字符串字面值都擁有 'static 生命周期,我們也可以選擇像下面這樣標(biāo)注出來:

let s: &'static str = "I have a static lifetime.";

這個(gè)字符串的文本被直接儲(chǔ)存在程序的二進(jìn)制文件中而這個(gè)文件總是可用的。因此所有的字符串字面值都是 'static 的。

你可能在錯(cuò)誤信息的幫助文本中見過使用 'static 生命周期的建議,不過將引用指定為 'static 之前,思考一下這個(gè)引用是否真的在整個(gè)程序的生命周期里都有效。你也許要考慮是否希望它存在得這么久,即使這是可能的。大部分情況,代碼中的問題是嘗試創(chuàng)建一個(gè)懸垂引用或者可用的生命周期不匹配,請(qǐng)解決這些問題而不是指定一個(gè) 'static 的生命周期。

結(jié)合泛型類型參數(shù)、trait bounds 和生命周期

讓我們簡(jiǎn)要的看一下在同一函數(shù)中指定泛型類型參數(shù)、trait bounds 和生命周期的語(yǔ)法!

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

這個(gè)是示例 10-22 中那個(gè)返回兩個(gè)字符串 slice 中較長(zhǎng)者的 longest 函數(shù),不過帶有一個(gè)額外的參數(shù) ann。ann 的類型是泛型 T,它可以被放入任何實(shí)現(xiàn)了 where 從句中指定的 Display trait 的類型。這個(gè)額外的參數(shù)會(huì)使用 {} 打印,這也就是為什么 Display trait bound 是必須的。因?yàn)樯芷谝彩欠盒停陨芷趨?shù) 'a 和泛型類型參數(shù) T 都位于函數(shù)名后的同一尖括號(hào)列表中。

總結(jié)

這一章介紹了很多的內(nèi)容!現(xiàn)在你知道了泛型類型參數(shù)、trait 和 trait bounds 以及泛型生命周期類型,你已經(jīng)準(zhǔn)備好編寫既不重復(fù)又能適用于多種場(chǎng)景的代碼了。泛型類型參數(shù)意味著代碼可以適用于不同的類型。trait 和 trait bounds 保證了即使類型是泛型的,這些類型也會(huì)擁有所需要的行為。由生命周期注解所指定的引用生命周期之間的關(guān)系保證了這些靈活多變的代碼不會(huì)出現(xiàn)懸垂引用。而所有的這一切發(fā)生在編譯時(shí)所以不會(huì)影響運(yùn)行時(shí)效率!

你可能不會(huì)相信,這個(gè)話題還有更多需要學(xué)習(xí)的內(nèi)容:第十七章會(huì)討論 trait 對(duì)象,這是另一種使用 trait 的方式。還有更多更復(fù)雜的涉及生命周期注解的場(chǎng)景,只有在非常高級(jí)的情況下才會(huì)需要它們;對(duì)于這些內(nèi)容,請(qǐng)閱讀 Rust Reference。不過接下來,讓我們聊聊如何在 Rust 中編寫測(cè)試,來確保代碼的所有功能能像我們希望的那樣工作!


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)