This article is part of the Rust學習筆記 series.

壽元是Rust因對堆棧的抽象及保證內存安全而生的(所有權外的)另一概念。

基本而言,壽元代表的是值的有效範圍。許多語言(如C++)中的作用域即是壽元的一個方面,但Rust中的壽元是一更通用/抽象且明確要求的概念。

我見到不少中文Rust相關材料中將lifetime翻譯爲「生命週期」,實在是令人瞠目結舌:「生命週期」是對lifecycle的翻譯,而這些人似乎完全沒有看到Rust中這個概念是lifetime

當然,完全存在另一種可能:他們看到了,仍然將其翻譯爲「生命週期」。這樣則是體現了當代中文翻譯中的一大問題:墨守成規和懶惰——固守已有的「詞」,寧可用錯也不願意另外造詞。當然這也是環境造就的,畢竟中文本以字爲基礎,但一則現在的教育讓人們習慣以詞(字組)爲基礎,二則現在通行的漢字數字化方案對新造漢字很不友好

「壽元」未必是最優的翻譯,但卻是我現下能想到的最信達雅的翻譯。如果有達成共識的不更差的翻譯,我個人很願意遷移。

Rust中,對壽元的需求存在於引用(借用)之上——只有引用的值纔會有值的壽元不同於變量壽元的情況,而所擁有的值壽元永遠和變量一致。理論上來說,每一引用均包含壽元,且每個引用的壽元均會被檢查;但出於方便考慮,部分情況下壽元不需要顯式標明(見後文)。

由於行文順序,此章節不討論自定義數據類型(主要是結構體)上壽元的使用,而是將其放在後面相應章節(雖然其本質即爲本章節所討論內容)。

壽元與所有權

所有權一節介紹過,值的所有權僅歸屬於一個變量,其他變量只能對其進行借用(或複製產生新值)。Rust規定的壽元和其所歸屬變量的壽元是統一的。因而,如下代碼不合法:

{
    let r;

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

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

其原因就是x的值(5)的壽元和其所歸屬的變量(x)的壽元一樣長,離開作用域(代碼塊)後就失效。Rust編譯器的報錯很明確地說明該問題:

error[E0597]: `x` does not live long enough
  --> src/main.rs:7:5
   |
6  |         r = &x;
   |              - borrow occurs here
7  |     }
   |     ^ `x` dropped here while still borrowed
...
10 | }
   | - borrowed value needs to live until here

可以看到,將壽元概念明確化可以讓原本一些運行時的錯誤得以在編譯期檢查出來。當然,壽元的概念並不僅於此,這只是其最簡單的例子。

使用壽元最多的地方在函數簽名中。在介紹使用之前,首先介紹壽元的語法及語義。

壽元語法及語義

壽元僅用在引用之上,所以僅當變量是引用類型時纔可能需要聲明壽元。壽元的標識是單引號(')加正常關鍵字名,通常僅用小寫字母且非常短;壽元標識放在引用符號之後,且和類型之間用空格分隔開來。

&i32        // 引用
&'a i32     // 具有顯式壽元的引用
&'a mut i32 // 具有顯式壽元的可變引用

需要注意的是,壽元聲明不影響值的壽元,而僅僅是標明其壽元(以幫助編譯器和編程人員)——值的實際使用是唯一影響其實際壽元的途徑/原因。因而,如果標明的壽元和實際壽元有衝突,編譯器會直接報錯。

如前文所說,壽元是基於「借用」這一概念的,所以考慮壽元時可以用類似的方法,也可以從堆棧的角度考慮(但壽元實際上帶來了更細粒度的控制)。

多數情況下,如果是在同一代碼塊內對變量進行借用,壽元往往不需要顯式聲明。而進行函數定義時,由於有通用性考慮,往往需要顯式聲明壽元。

函數簽名

顯式的壽元使用最廣泛的兩處之一便是函數簽名部分(另一處是自定義數據類型,尤結構體)。

其形式爲:

fn max<'a>(num1: &'a i32, num2: &'a i32) -> &'a i32 {
    if num1 > num2 {
        num1
    } else {
        num2
    }
}

fn main() {
   let a = 3;
   let b = 5;
   let c = max(&a, &b);
   print!("{}", c);
}

在函數名之後參數表之前,放置一對尖括號,其內聲明會用到的壽元(用逗號分隔),然後在參數類型和返回值類型上使用。該語法同時也是Rust中指明泛型類型參數的語法(壽元和類型參數放在一起)。

本例中,該函數直接返回兩參數中的一個,所以返回值的壽元和參數的壽元一致(更嚴格地說:返回值所借用的值,參數所借用的值)。在實際使用中要自己考慮清楚具體的壽元,並且一般應以最小標註爲標準。

爲簡便起見,在符合規則的情況下可以不指明部分參數的壽元(編譯器會自動進行嘗試)。但該「規則」並不具有理論上的統一性,而僅是因爲他們很「常見」(所以可能還會增加)。官方教程的這個部分對此進行了一點討論。

可以看到,壽元機制使得編譯器可以檢查函數返回的值(在返回後)是否仍然有效,並且會持續追蹤。這使得懸空指針的問題被徹底避免掉,而且是在編譯期即可檢查出來(且不要忘記Rust的編譯速度很快)。

Renyuneyun

Arch Linux用戶;閒暇時爲FLOSS做做貢獻;認同自由軟件理念。自認唯物論者;反對任意形式的迷信;在意社會問題;拒絕先入爲主。

Renyuneyun

Join the discussion