這篇指南是 Rust 已經(jīng)存在的三個所有權(quán)制度之一。這是 Rust 最獨(dú)特和最令人信服的一個特點(diǎn),Rust 開發(fā)人員應(yīng)該相當(dāng)熟悉。所有權(quán)即 Rust 如何實(shí)現(xiàn)其最大目標(biāo)和內(nèi)存安全。這里有幾個不同的概念,每一個概念都有它自己的章節(jié):
這三篇文章相關(guān)且有序。如果你想完全的理解所有權(quán)制度,你將需要這三篇文章。
在我們了解細(xì)節(jié)之前,這里有關(guān)于所有權(quán)制度的兩個重要說明需要知道。
Rust 注重安全和速度。它通過許多‘零成本抽象’來完成這些目標(biāo),這意味著在 Rust 中,用盡可能少的抽象成本來保證它們正常工作。所有權(quán)制度是一個零成本抽象概念的一個主要例子。我們將在這篇指南中提到的所有分析都是在編譯時完成的。你不用為了任何功能花費(fèi)任何運(yùn)行成本。
然而,這一制度確實(shí)需要一定的成本:學(xué)習(xí)曲線。許多新用戶使用我們喜歡稱之為‘與借檢查器的人斗爭’,即 Rust 編譯器拒絕編譯那些作者認(rèn)為有效的程序的 Rust 經(jīng)驗(yàn)。這往往因?yàn)槌绦騿T對于所有權(quán)的怎樣工作與 Rust 實(shí)現(xiàn)的規(guī)則不相符的心理模型而經(jīng)常出現(xiàn)。你可能在第一次時會經(jīng)歷類似的事情。然而有個好消息:更有經(jīng)驗(yàn)的 Rust 開發(fā)者報(bào)告稱,一旦他們遵從所有權(quán)制度的規(guī)則工作一段時間后,他們會越來越少的與借檢查器的行為斗爭。
學(xué)習(xí)了這些后,讓我們來了解生存期。
借出一個對于其他人已經(jīng)擁有的資源的引用會很復(fù)雜。例如,假設(shè)這一系列的操作:
我獲得某種資源的一個句柄。
我借給你對于這個資源的引用。
我決定我使用完了這個資源,然后釋放它,然而你仍然擁有這個引用。
你的引用指向一個無用的資源。當(dāng)資源是內(nèi)存時,這被稱為懸掛指針或者‘釋放后再利用’。
要解決此類問題,我們必須確保在第三步后一定不會發(fā)生第四步。Rust 的所有權(quán)制度通過被稱為生存期的一章來實(shí)現(xiàn),生存期用來介紹一個引用的有效的作用域。
當(dāng)我們有一個函數(shù)將一個引用作為參數(shù)時,我們可以用隱式和顯式兩種方式來表示引用的生存期:
// implicit
fn foo(x: &i32) {
}
// explicit
fn bar<'a>(x: &'a i32) {
}
'a 讀‘生存期為 a ’。從技術(shù)上講,每個引用都有一些與之關(guān)聯(lián)的生存期,但是編譯器允許你在一般情況下忽略它們。但是在此之前,以下是我們分解顯式例子的代碼:
fn bar<'a>(...)
這一部分聲明了我們的生存期。這說明 bar 有一個生存期 'a。如果我們有兩個引用的參數(shù),以下是相關(guān)代碼:
fn bar<'a, 'b>(...)
然后在我們的參數(shù)列表中,我們使用我們已經(jīng)命名的生存期。
...(x: &'a i32)
如果我們想要一個 &mut 引用,我們可以書寫以下代碼:
...(x: &'a mut i32)
如果你將 &mut i32 和 &'a mut i32 比較,它們是相同的,只是在 & 和 mut i32 之間多了一個 'a。我們將 &mut i32 讀作‘對于 i32 的一個可變引用’,將 &'a mut i32讀作‘對于 i32 的一個生存期為'a 的一個可變引用’。
當(dāng)你操作結(jié)構(gòu)體時,也需要顯式的生存期:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // this is the same as `let _y = 5; let y = &_y;`
let f = Foo { x: y };
println!("{}", f.x);
}
正如你所看到的的,結(jié)構(gòu)體也可以有生存期。與函數(shù)相似的方式,
struct Foo<'a> {
聲明一個生存期,和以下代碼
x: &'a i32,
使用它。所以,為什么我們在這里需要一個生存期?我們需要確保對 Foo 的任何引用都不能比對它包含的 i32 的引用的壽命長。
考慮生存期的一種方式是將一個引用的有效作用域可視化。例如:
fn main() {
let y = &5; // -+ y goes into scope
// |
// stuff// |
// |
} // -+ y goes out of scope
在我們的 Foo 中添加:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // -+ y goes into scope
let f = Foo { x: y }; // -+ f goes into scope
// stuff // |
// |
} // -+ f and y go out of scope
我們的 f 在 y 的作用域內(nèi)存活,所以一切正常。如果它不是呢?這個代碼不會有效工作:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x;// -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x);// |
} // -+ x goes out of scope
正如你所看到的,f 和 y 的作用域比 x 的作用域要小。但是,當(dāng)我們運(yùn)行 x = &f.x 時,我們給了 x 一個可以超出其作用域范圍的引用。
命名生存期是給這些作用域命名的一種方式。給東西命名是能不能討論它的第一步。
命名為 ‘static’ 的生存期是一個特殊的生存期。這標(biāo)志著這種東西具有整個程序的生存期。很多的 Rust 程序員在處理字符串時會第一次遇到 'static:
let x: &'static str = "Hello, world.";
字符串具有 &'static str 這種類型,是因?yàn)檫@種引用始終存在:它們?nèi)谌氲阶罱K二進(jìn)制的數(shù)據(jù)段中。另一個例子是全局變量:
static FOO: i32 = 5;
let x: &'static i32 = &FOO;
以上代碼是將 i32 加入到二進(jìn)制文件的數(shù)據(jù)段中,其中 x 是它的一個引用。
Rust 在函數(shù)體中支持強(qiáng)大的局部類型推斷,但是它在項(xiàng)目簽名中禁止允許僅僅基于單獨(dú)的項(xiàng)目簽名中的類型推斷。然而,對于人體工學(xué)推理來說被稱為“生存期省略”的一個非常受限的二級推理算法,對于函數(shù)簽名非常適用。它能推斷僅僅基于簽名組件本身而不是基于函數(shù)體,僅推斷生存期參數(shù),并且通過僅僅三個容易記住和明確的規(guī)則來實(shí)現(xiàn),這使得生存期省略成為一個項(xiàng)目簽名的縮寫,而不是引用它之后隱藏包含完整的本地推理的實(shí)際類型。
當(dāng)我們談到生存期省略時,我們使用生存期輸入和生存期輸出 這兩個術(shù)語。生存期輸入是與一個函數(shù)的一個參數(shù)結(jié)合的一個生存期,同時一個生存期輸出是一個與函數(shù)的返回值相結(jié)合的一個生存期。例如,以下函數(shù)有一個生存期輸入:
fn foo<'a>(bar: &'a str)
以下函數(shù)有一個生存期輸出:
fn foo<'a>() -> &'a str
以下函數(shù)在兩個位置都有一個生存期:
fn foo<'a>(bar: &'a str) -> &'a str
這里有三個規(guī)定:
在函數(shù)參數(shù)中每個省略的生存期都成為一個獨(dú)特的生存期參數(shù)。
如果僅僅有一個輸入生存期,省略或者不省略,在這個函數(shù)的返回值中,這個生存期被分配給所有的省略的生存期。
另外,省略一個生存期輸出是錯誤的。
以下列舉了生存期省略的函數(shù)的一些例子。我們已經(jīng)將每個生存期省略的例子和它的擴(kuò)展形式進(jìn)行了配對。
fn print(s: &str); // elided
fn print<'a>(s: &'a str); // expanded
fn debug(lvl: u32, s: &str); // elided
fn debug<'a>(lvl: u32, s: &'a str); // expanded
// In the preceding example, `lvl` doesn’t need a lifetime because it’s not a
// reference (`&`). Only things relating to references (such as a `struct`
// which contains a reference) need lifetimes.
fn substr(s: &str, until: u32) -> &str; // elided
fn substr<'a>(s: &'a str, until: u32) -> &'a str; // expanded
fn get_str() -> &str; // ILLEGAL, no inputs
fn frob(s: &str, t: &str) -> &str; // ILLEGAL, two inputs
fn frob<'a, 'b>(s: &'a str, t: &'b str) -> &str; // Expanded: Output lifetime is unclear
fn get_mut(&mut self) -> &mut T; // elided
fn get_mut<'a>(&'a mut self) -> &'a mut T; // expanded
fn args<T:ToCStr>(&mut self, args: &[T]) -> &mut Command // elided
fn args<'a, 'b, T:ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // expanded
fn new(buf: &mut [u8]) -> BufWriter; // elided
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a> // expanded