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"
的引用賦給了 Foo
的 name
成員變數,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 的報錯,而是想當然地認為這可能跟 a
和 b
的生命週期不一致有關。現在,可以給生命週期翻案了。
上述程式碼的錯誤之處在於,a
被 b
可變借用了,而且是可變借用,而在隨後的 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);
}
小結
不再太害怕生命週期標記了。