鍍金池/ 教程/ Java/ 生命周期( Lifetime )
標(biāo)準(zhǔn)輸入與輸出
消息傳遞
循環(huán)
注釋
Rust for Mac OS
幾種智能指針
Cell, RefCell
trait對象 (trait object)
rust web 開發(fā)
Unsafe、原始指針
Macro
迭代器
函數(shù)
Borrow, BorrowMut, ToOwned
快速上手
二叉樹
編輯器
測試與評測
Deref
安裝Rust
哈希表 HashMap
原生類型
17.錯誤處理
VS Code 安裝配置
動態(tài)數(shù)組Vec
模式匹配
操作符和格式化字符串
Rust for Linux
函數(shù)參數(shù)
Visual Studio
vim/GVim安裝配置
閉包作為參數(shù)和返回值
安全(Safety)
Cow
生命周期( Lifetime )
閉包的實現(xiàn)
所有權(quán)(Ownership)
Atom
將Rust編譯成庫
類型、運算符和字符串
類型系統(tǒng)中的幾個常見 trait
特性
屬性和編譯器參數(shù)
Spacemacs
集合類型
Rust json處理
Heap & Stack
并行
標(biāo)準(zhǔn)庫示例
基本程序結(jié)構(gòu)
鏈表
trait 和 trait對象
前期準(zhǔn)備
代碼風(fēng)格
編譯器參數(shù)
基于語義化版本的項目版本聲明與管理
Rust 版本管理工具: rustup
引用&借用(References&Borrowing)
注釋與文檔
10.1 trait關(guān)鍵字
模式
調(diào)用ffi函數(shù)
unsafe
并發(fā),并行,多線程編程
AsRef 和 AsMut
Rust旅程
Rust for Windows
結(jié)構(gòu)體與枚舉
條件分支
附錄I-術(shù)語表
變量綁定與原生類型
Mutex 與 RwLock
泛型
裸指針
常用數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
系統(tǒng)命令:調(diào)用grep
Into/From 及其在 String 和 &str 互轉(zhuǎn)上的應(yīng)用
共享內(nèi)存
Sublime
網(wǎng)絡(luò)模塊:W貓的回音
函數(shù)返回值
包和模塊
高階函數(shù)
函數(shù)與方法
match關(guān)鍵字
隊列
目錄操作:簡單grep
語句和表達式
并發(fā)編程
閉包
測試
閉包的語法
同步
迭代器
String
Send 和 Sync
Rc 和 Arc
屬性
Emacs
優(yōu)先隊列
Prelude
cargo簡介
控制流(control flow)
數(shù)組、動態(tài)數(shù)組和字符串
FFI
模塊和包系統(tǒng)、Prelude
實戰(zhàn)篇
Rust 是一門系統(tǒng)級編程語言,被設(shè)計為保證內(nèi)存和線程安全,并防止段錯誤。作為系統(tǒng)級編程語言,它的基本理念是 “零開銷抽象”。理
運算符重載
Any和反射
rust數(shù)據(jù)庫操作
輸入輸出流
復(fù)合類型
性能測試

生命周期( Lifetime )

下面是一個資源借用的例子:

fn main() {
    let a = 100_i32;

    {
        let x = &a;
    }  // x 作用域結(jié)束
    println!("{}", x);
}

編譯時,我們會看到一個嚴(yán)重的錯誤提示:

error: unresolved name x.

錯誤的意思是“無法解析 x 標(biāo)識符”,也就是找不到 x , 這是因為像很多編程語言一樣,Rust中也存在作用域概念,當(dāng)資源離開離開作用域后,資源的內(nèi)存就會被釋放回收,當(dāng)借用/引用離開作用域后也會被銷毀,所以 x 在離開自己的作用域后,無法在作用域之外訪問。

上面的涉及到幾個概念:

  • Owner: 資源的所有者 a
  • Borrower: 資源的借用者 x
  • Scope: 作用域,資源被借用/引用的有效期

強調(diào)下,無論是資源的所有者還是資源的借用/引用,都存在在一個有效的存活時間或區(qū)間,這個時間區(qū)間稱為生命周期, 也可以直接以Scope作用域去理解。

所以上例子代碼中的生命周期/作用域圖示如下:

            {    a    {    x    }    *    }
所有者 a:         |________________________|
借用者 x:                   |____|            x = &a
  訪問 x:                             |       失敗:訪問 x

可以看到,借用者 x 的生命周期是資源所有者 a 的生命周期的子集。但是 x 的生命周期在第一個 } 時結(jié)束并銷毀,在接下來的 println! 中再次訪問便會發(fā)生嚴(yán)重的錯誤。

我們來修正上面的例子:

fn main() {
    let a = 100_i32;

    {
        let x = &a;
        println!("{}", x);
    }  // x 作用域結(jié)束

}

這里我們僅僅把 println! 放到了中間的 {}, 這樣就可以在 x的生命周期內(nèi)正常的訪問 x ,此時的Lifetime圖示如下:

            {    a    {    x    *    }    }
所有者 a:         |________________________|
借用者 x:                   |_________|       x = &a
  訪問 x:                        |            OK:訪問 x

隱式Lifetime

我們經(jīng)常會遇到參數(shù)或者返回值為引用類型的函數(shù):

fn foo(x: &str) -> &str {
    x
}

上面函數(shù)在實際應(yīng)用中并沒有太多用處,foo 函數(shù)僅僅接受一個 &str 類型的參數(shù)(x為對某個string類型資源Something的借用),并返回對資源Something的一個新的借用。

實際上,上面函數(shù)包含該了隱性的生命周期命名,這是由編譯器自動推導(dǎo)的,相當(dāng)于:

fn foo<'a>(x: &'a str) -> &'a str {
    x
}

在這里,約束返回值的Lifetime必須大于或等于參數(shù)x的Lifetime。下面函數(shù)寫法也是合法的:

fn foo<'a>(x: &'a str) -> &'a str {
    "hello, world!"
}

為什么呢?這是因為字符串"hello, world!"的類型是&'static str,我們知道static類型的Lifetime是整個程序的運行周期,所以她比任意傳入的參數(shù)的Lifetime'a都要長,即'static >= 'a滿足。

在上例中Rust可以自動推導(dǎo)Lifetime,所以并不需要程序員顯式指定Lifetime 'a 。

'a是什么呢?它是Lifetime的標(biāo)識符,這里的a也可以用b、c、d、e、...,甚至可以用this_is_a_long_name等,當(dāng)然實際編程中并不建議用這種冗長的標(biāo)識符,這樣會嚴(yán)重降低程序的可讀性。foo后面的<'a>為Lifetime的聲明,可以聲明多個,如<'a, 'b>等等。

另外,除非編譯器無法自動推導(dǎo)出Lifetime,否則不建議顯式指定Lifetime標(biāo)識符,會降低程序的可讀性。

顯式Lifetime

當(dāng)輸入?yún)?shù)為多個借用/引用時會發(fā)生什么呢?

fn foo(x: &str, y: &str) -> &str {
    if true {
        x
    } else {
        y
    }
}

這時候再編譯,就沒那么幸運了:

error: missing lifetime specifier [E0106]
fn foo(x: &str, y: &str) -> &str {
                            ^~~~

編譯器告訴我們,需要我們顯式指定Lifetime標(biāo)識符,因為這個時候,編譯器無法推導(dǎo)出返回值的Lifetime應(yīng)該是比 x長,還是比y長。雖然我們在函數(shù)中中用了 if true 確認(rèn)一定可以返回x,但是要知道,編譯器是在編譯時候檢查,而不是運行時,所以編譯期間會同時檢查所有的輸入?yún)?shù)和返回值。

修復(fù)后的代碼如下:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

Lifetime推導(dǎo)

要推導(dǎo)Lifetime是否合法,先明確兩點:

  • 輸出值(也稱為返回值)依賴哪些輸入值
  • 輸入值的Lifetime大于或等于輸出值的Lifetime (準(zhǔn)確來說:子集,而不是大于或等于)

Lifetime推導(dǎo)公式: 當(dāng)輸出值R依賴輸入值X Y Z ...,當(dāng)且僅當(dāng)輸出值的Lifetime為所有輸入值的Lifetime交集的子集時,生命周期合法。

    Lifetime(R) ? ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )

對于例子1:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

因為返回值同時依賴輸入?yún)?shù)xy,所以

    Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

    即:

    'a ? ('a ∩ 'a)  // 成立

定義多個Lifetime標(biāo)識符

那我們繼續(xù)看個更復(fù)雜的例子,定義多個Lifetime標(biāo)識符:

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

先看下編譯,又報錯了:

<anon>:5:3: 5:4 error: cannot infer an appropriate lifetime for automatic coercion due to conflicting requirements [E0495]
<anon>:5        y
                ^
<anon>:1:1: 7:2 help: consider using an explicit lifetime parameter as shown: fn foo<'a>(x: &'a str, y: &'a str) -> &'a str
<anon>:1 fn bar<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
<anon>:2    if true {
<anon>:3        x
<anon>:4    } else {
<anon>:5        y
<anon>:6    }

編譯器說自己無法正確地推導(dǎo)返回值的Lifetime,讀者可能會疑問,“我們不是已經(jīng)指定返回值的Lifetime為'a了嗎?"。

這兒我們同樣可以通過生命周期推導(dǎo)公式推導(dǎo):

因為返回值同時依賴xy,所以

    Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

    即:

    'a ? ('a ∩ 'b)  //不成立

很顯然,上面我們根本沒法保證成立。

所以,這種情況下,我們可以顯式地告訴編譯器'b'a長('a'b的子集),只需要在定義Lifetime的時候, 在'b的后面加上: 'a, 意思是'b'a長,'a'b的子集:

fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

這里我們根據(jù)公式繼續(xù)推導(dǎo):

    條件:Lifetime(x) ? Lifetime(y)
    推導(dǎo):Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

    即:

    條件: 'a ? 'b
    推導(dǎo):'a ? ('a ∩ 'b) // 成立

上面是成立的,所以可以編譯通過。

推導(dǎo)總結(jié)

通過上面的學(xué)習(xí)相信大家可以很輕松完成Lifetime的推導(dǎo),總之,記住兩點:

  1. 輸出值依賴哪些輸入值。
  2. 推導(dǎo)公式。

Lifetime in struct

上面我們更多討論了函數(shù)中Lifetime的應(yīng)用,在struct中Lifetime同樣重要。

我們來定義一個Person結(jié)構(gòu)體:

struct Person {
    age: &u8,
}

編譯時我們會得到一個error:

<anon>:2:8: 2:12 error: missing lifetime specifier [E0106]
<anon>:2    age: &str,

之所以會報錯,這是因為Rust要確保Person的Lifetime不會比它的age借用長,不然會出現(xiàn)Dangling Pointer的嚴(yán)重內(nèi)存問題。所以我們需要為age借用聲明Lifetime:

struct Person<'a> {
    age: &'a u8,
}

不需要對Person后面的<'a>感到疑惑,這里的'a并不是指Person這個struct的Lifetime,僅僅是一個泛型參數(shù)而已,struct可以有多個Lifetime參數(shù)用來約束不同的field,實際的Lifetime應(yīng)該是所有fieldLifetime交集的子集。例如:

fn main() {
    let x = 20_u8;
    let stormgbs = Person {
                        age: &x,
                     };
}

這里,生命周期/Scope的示意圖如下:

                  {   x    stormgbs      *     }
所有者 x:              |________________________|
所有者 stormgbs:                |_______________|  'a
借用者 stormgbs.age:            |_______________|  stormgbs.age = &x

既然<'a>作為Person的泛型參數(shù),所以在為Person實現(xiàn)方法時也需要加上<'a>,不然:

impl Person {
    fn print_age(&self) {
        println!("Person.age = {}", self.age);
    }
}

報錯:

<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl Person {
              ^~~~~~

正確的做法是

impl<'a> Person<'a> {
    fn print_age(&self) {
        println!("Person.age = {}", self.age);
    }
}

這樣加上<'a>后就可以了。讀者可能會疑問,為什么print_age中不需要加上'a?這是個好問題。因為print_age的輸出參數(shù)為(),也就是可以不依賴任何輸入?yún)?shù), 所以編譯器此時可以不必關(guān)心和推導(dǎo)Lifetime。即使是fn print_age(&self, other_age: &i32) {...}也可以編譯通過。

如果Person的方法存在輸出值(借用)呢?

impl<'a> Person<'a> {
    fn get_age(&self) -> &u8 {
        self.age
    }
}

get_age方法的輸出值依賴一個輸入值&self,這種情況下,Rust編譯器可以自動推導(dǎo)為:

impl<'a> Person<'a> {
    fn get_age(&'a self) -> &'a u8 {
        self.age
    }
}

如果輸出值(借用)依賴了多個輸入值呢?

impl<'a, 'b> Person<'a> {
    fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {
        if self.age > p.age {
            self.age
        } else {
            p.age
        }
    }
}

類似之前的Lifetime推導(dǎo)章節(jié),當(dāng)返回值(借用)依賴多個輸入值時,需顯示聲明Lifetime。和函數(shù)Lifetime同理。

其他

無論在函數(shù)還是在struct中,甚至在enum中,Lifetime理論知識都是一樣的。希望大家可以慢慢體會和吸收,做到舉一反三。

總結(jié)

Rust正是通過所有權(quán)、借用以及生命周期,以高效、安全的方式近乎完美地管理了內(nèi)存。沒有手動管理內(nèi)存的負載和安全性,也沒有GC造成的程序暫停問題。

上一篇:下一篇:閉包