參照與借用
本指南是當前 Rust 的三個所有權系統之一。 這是 Rust 最獨特且引人注目的功能之一,作為 Rust 的開發者應該對此要有相當的了解。 所有權是 Rust 用來達成其最大的目標,記憶體安全,的方法。 它有幾個不同的概念,各自有各自的章節:
這三章依序相關。 你需要了解全部三章來完整了解所有權系統。
Meta
在我們開始細述前,有兩個所有權系統的重點。
Rust 注重安全和速度。 它透過許多「零成本抽象化」的方式去實現目標,也就是 Rust 將盡可能縮小抽象化成本去達成目標。 所有權系統是零成本抽象化的一個最佳範例。 我們在本指南中談到的所有分析,都是在 編譯期完成的。 這些功能不需要花費你任何執行期的成本。
然而,這套系統仍有某些成本:學習曲線。 許多 Rust 的新使用者會經歷我們所說的「與借用檢查器(borrow checker)苦戰」的經驗,像是 Rust 編譯器無法編譯作者認為合理的程式。 這常常在程式設計師內心的所有權運作模型與實際的 Rust 實作不相符的時候會發生。 一開始你可能也會經歷類似的事情。 然而有個好消息是:許多有經驗的 Rust 開發者回報,當他們適應所有權系統的規則一陣子之後,他們跟借用檢查器的苦戰就越來越少了。
記住這些之後,讓我們開始學習借用吧。
借用(Borrowing)
在 所有權 一節的最後,我們提到一個看起來頗糟的函式:
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
這不符合 Rust 的語言習慣,因為它沒有利用到借用的優點。 以下是第一步:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
// return the answer
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(&v1, &v2);
// we can use v1 and v2 here!
與其把 Vec<i32>
作為我們的參數,不如使用參照(reference):&Vec<i32>
。
而且不要直接傳遞 v1
與 v2
,我們傳遞 &v1
與 &v2
。
我們把 &T
型別稱為「參照」(reference),它借用了所有權,而非掌握所有權。
一個借用東西的綁定不會在離開有效範圍時把資源釋放掉。
這代表在呼叫 foo()
完之後,我們仍可再度使用我們原始的綁定。
跟綁定一樣,reference 是不可變的(immutable)。
這意味著在 foo()
內,向量完全不能更改:
fn foo(v: &Vec<i32>) {
v.push(5);
}
let v = vec![];
foo(&v);
試著更改會出現以下錯誤:
error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^
因為放入一個值會改變向量,所以我們不被允許這樣做。
&mut reference
還有第二種 reference:&mut T
。
一個「可變的 reference」允許你改變所借用的資源。
例如:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
這將會印出 6
。
我們將 y
作為一個指到 x
的可變 reference,然後對 y
指向的東西加一。
你將注意到 x
也必須被標註為 mut
。
如果不這樣的話,我們將無法建立一個指向不可變的值的可變 reference。
譯註:如果
x
不是mut
,會得到error: cannot borrow immutable local variable
xas mutable
錯誤訊息。
你同時也會發現我們在 y
前面加上星號(*
)成為 *y
,這是因為 y
是一個 &mut
reference,你需要使用它們去存取 reference 的內容。
此外,&mut
reference 如同一般的 reference。它們兩者和它們的互動方式 有著 巨大的區別。
以上的範例可以說是有點不可靠的,因為我們需要額外的 {
和 }
來定義有效區域。
如果移除它們,我們會得到錯誤訊息:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
^
note: previous borrow ends here
fn main() {
}
^
事實證明如此,所以以下是一些規則。
規則
以下是 Rust 中關於借用(borrowing)的規則:
首先,任何借用的有效範圍都必須比擁有者的有效範圍還要小。 其次,你可以使用以下兩種借用,但是不能同時使用兩者:
- 一到多個對資源的 reference(
&T
) - 唯一一個可變 reference(
&mut T
)
你可能有注意到有點類似、但不完全相同於資料競爭的定義:
當兩個或多個指標同時存取相同的記憶體,其中至少有一個正在寫入,且操作沒有同步時,會出現「資料競爭」(date race)。
使用 references 時,你想用多少個都可以,因為它們都沒有寫入的行為。
然而,一次只能擁有一個 &mut
,才不會產生資料競爭。
當我們違反規則時,會得到錯誤訊息,這就是 Rust 如何在編譯期預防資料競爭。
記住這些之後,讓我們再次想想我們的範例。
對有效範圍的深思(Thinking in scopes)
以下是程式碼:
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
這段程式碼會有這些錯誤訊息:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
譯註:此處
println!()
試圖借用x
。
這是因為我們違反了規則:我們有一個指向 x
的 &mut T
,所以我們不被允許建立任何其他 &T
。
必須要在兩者間做出選擇。
錯誤訊息中的註解會提示我們該如何思考這個問題:
note: previous borrow ends here
fn main() {
}
^
換句話說,可變借用(mutable borrow)在我們的範例中一直存在。
所以我們希望可變借用能在我們呼叫 println!
並建立不可變借用 之前 能結束掉。
在 Rust 中,借用會綁定在借用的有效範圍中。
我們的有效範圍看起來會像這樣:
let mut x = 5;
let y = &mut x; // -+ &mut borrow of x starts here
// |
*y += 1; // |
// |
println!("{}", x); // -+ - try to borrow x here
// -+ &mut borrow of x ends here
這些有效範圍有衝突,因為我們不能在 y
仍在有效範圍時建立 &x
。
當增加大括號之後:
let mut x = 5;
{
let y = &mut x; // -+ &mut borrow starts here
*y += 1; // |
} // -+ ... and ends here
println!("{}", x); // <- try to borrow x here
這樣就沒有問題了。 我們的可變借用會在我們建立不可變借用前離開有效範圍。 有效範圍是個看清借用持續多久的關鍵。
借用所預防的問題(Issues borrowing prevents)
為何我們需要這些限制規則? 好吧,如同我們所說的,這些規則避免資料競爭。 資料競爭會引發哪些問題? 以下是一些例子。
疊代器失效
一個例子是「疊代器失效」(Iterator invalidation),當你試圖改變一個正在疊代的集合(collection)時會發生。 Rust 的借用檢查器會預防這件事發生:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
這會印出一到三。
當我們疊代這個向量時,我們只會被給予其中元素的 references。
而且 v
是一個不可變的借用,所以我們在疊代時不能更改它:
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
v.push(34);
}
以下是錯誤訊息:
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^
我們不能修改 v
,因為它已經被借給迴圈了。
在釋放之後使用(use after free)
References 不能存活得比所參考的資源還久。 Rust 會檢查你的 references 的有效範圍來確保符合這個條件。
如果 Rust 沒有檢查這個屬性,我們可能會意外地用到一個無效的 reference。 例如:
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
會得到以下錯誤:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
換句話說,y
只在 x
存在的有效範圍內有效。
當 x
消失,它就成為無效的 reference。
這個錯誤訊息說,這個借用「存在得不夠久」就是因為它在還應該存在的時候就已經失效了。
當一個 reference 在它所參考的變數 之前 宣告,也會發生一樣的問題。 這是因為當資源在同樣的有效範圍之內時,它們被釋放的順序會跟它們的宣告順序相反:
譯註:也就是
宣告 y; 宣告 x;
的話,離開有效範圍時會釋放 x; 釋放 y
。所以y
比x
存活的久。
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
得到以下錯誤訊息:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}
在以上範例,y
在 x
之前宣告,代表 y
存活的比 x
還長,這不被允許。
commit 6ba9520