與 Rust 勾心鬥角 · 終於要標註生命週期了

garfileo發表於2022-05-21

rskynet 只是一個有著 300 餘行程式碼的小專案……或者極其渺小專案,所實現的功能是讀取記載多面體資訊的 OFF 檔案,計算多面體的包圍球,基於包圍球的中心半自動化的生成 POV Ray 場景中的模型和檢視檔案並交由 povray 解析,為多面體生成指定視角的渲染結果。不過,這些功能幾乎與本文無關,有關的僅僅是在實現這些功能的過程中,在為 Mesh 結構體定義一個方法時,遇到了需要顯式標註生命週期的情況。

問題要簡單化

假設有一個結構體

struct Foo {
    name: &str,
    value: i32,
}

若使用以下程式碼構造 Foo 的例項

let x = Foo {name: "foo", value: 1};

rustc 會無奈地指出

error[E0106]: missing lifetime specifier
  ... ... ...
  |
  |     name: &str,
  |           ^ expected named lifetime parameter

還會給出建議

help: consider introducing a named lifetime parameter
  |
  ~ struct Foo<'a> {
  ~     name: &'a str,
  |

按照上述建議,將 Foo 的定義修改為

struct Foo<'a> {
    name: &'a str,
    value: i32,
}

問題便得到了解決,至此我見證了 Rust 的偉大發明——生命週期標註,但是究竟發生了什麼?

生命週期是泛型型別

在修改後的 Foo 裡,'a 出現在我熟悉的泛型引數 T 出現的位置,它是泛型引數嗎?我猜,是的。萬物不同,但時間都是相同的。在 Rust 語言裡,每個引用都有生命週期。使用過期的引用(所引用的變數已被釋放),會導致程式碼被 rustc 拒絕通過。理想是好的,但 rustc 有時無法判斷一個引用是否過期。例如

let x = Foo {name: "foo", value: 1};

將字串 "foo" 的引用賦給了 Fooname 成員變數,rustc 無法確定在 x 的生命週期內,字串 "foo" 依然健在。在我看來,"foo" 是直接編碼在程式的可執行檔案裡的,它與程式同壽,完全沒有可能在 x 的生命週期內被釋放,但是 在 rustc 看來,它只知道這是個字串的引用,而只要是引用,就可能存在引用過期變數的危險,因此它需要我明確告訴它,"foo" 能活多久。

生命週期標記

要告訴 rustc,一個變數的生命週期是多久,基本是不可能的,但是能夠通過生命週期標記告訴 rustc,一個變數的生命週期至少與某個生命週期相等。例如

struct Foo<'a> {
    name: &'a str,
    value: i32,
}

能夠告訴 rustc,name 引用的變數至少能活得跟 Foo 的例項一樣久,事實上,的確如此。

生命週期標記僅僅是對一個引用的時效性給予約束,它並不能改變該引用的生命週期。下面的程式碼可以說明這一點,

let mut x = Foo {name: "", value: 1};
{
    let a = String::from("foo");
    foo.name = a.as_str();
}
println!("({}, {})", foo.name, foo.value);

a{ ... } 構成的區域性作用區域記憶體活,出了該區域,便會被釋放。儘管 a 的部分內容(字串切片)被 foo.name 引用,而且生命週期標記要求該部分內容的生命週期至少與 foo 相等,但實際情況並非如此,因此 rustc 會拒絕這段程式碼通過編譯,並給出以下錯誤資訊

error[E0597]: `a` does not live long enough
   ... ... ...
   |
   |         foo.name = a.as_str();
   |                    ^^^^^^^^^^ borrowed value does not live long enough
   |     }
   |     - `a` dropped here while still borrowed
   |     println!("({}, {})", foo.name, foo.value);
   |                          -------- borrow later used here

陳年冤案

去年的上個月在寫 rhamal.pdf,在 2.8 節,我遇到了一個靈異事件,即以下程式碼無法通過編譯:

struct Point {x: f64, y: f64, z: f64}

fn main() {
    let mut a = Point {x: 1.0, y: 2.0, z: 3.0};
    let b = &mut a;
    println!("({}, {}, {})", a.x, a.y, a.z);
    println!("({}, {}, {})", b.x, b.y, b.z);
}

當時,由於對 Rust 語法過於畏懼,以致沒有耐心觀察 rustc 的報錯,而是想當然地認為這可能跟 ab 的生命週期不一致有關。現在,可以給生命週期翻案了。

上述程式碼的錯誤之處在於,ab 可變借用了,而且是可變借用,而在隨後的 println! 語句中,a 是以不可變借用的形式出現——println! 是一個巨集,其引數會自動被轉化為不可變借用。在 Rust 語法裡,一個變數在被當成可變借用之後,倘若對其進行不可變借用,那麼就再也不能通過可變引用訪問它了,反之,若對一個變數先進行不可變借用,然後再進行可變借用,這是允許的——大概可以避免資料競爭。因此,上述程式碼需要修改為

struct Point {x: f64, y: f64, z: f64}

fn main() {
    let mut a = Point {x: 1.0, y: 2.0, z: 3.0};
    let b = &mut a;
    println!("({}, {}, {})", b.x, b.y, b.z);
    println!("({}, {}, {})", a.x, a.y, a.z);
}

小結

不再太害怕生命週期標記了。

相關文章