借用規則
本集目標
理解 Rust 的借用規則:同時只能有一個 &mut 或多個 &、不能 move 被借用的值,以及懸垂參考的問題。
概念說明
為什麼需要規則?
上一集我們學了 &mut 可變借用。但如果 Rust 允許你同時有多個可變參考,會怎樣?
想像你有一串鑰匙圈。借給很多人看(&)沒問題——大家都只是看,不會改變鑰匙圈上有什麼。但如果同時借給兩個人修改(&mut)——A 在加一把新鑰匙,B 同時在拆掉那把——結果就不可預測了。
這是對同一份資料互相衝突的存取,可能會導致各種奇怪的 bug。所以 Rust 制定了嚴格的借用規則。
規則一:同時只能有一個 &mut
在同一個時間點,一個值最多只能有一個可變參考:
#![allow(unused_variables)]
fn main() {
let mut x = 10;
let r1 = &mut x;
let r2 = &mut x; // 編譯錯誤!已經有一個 &mut 了
*r1 += 1;
}
規則二:& 和 &mut 不能同時存在
如果有人在讀(&),就不能有人在改(&mut);反過來也是:
#![allow(unused_variables)]
fn main() {
let mut x = 10;
let r1 = &x; // 唯讀借用
let r2 = &mut x; // 編譯錯誤!已經有 & 了,不能再 &mut
println!("{}", r1);
}
規則三:多個 & 可以同時存在
多個人同時讀,沒有任何問題:
fn main() {
let x = 10;
let r1 = &x;
let r2 = &x;
let r3 = &x;
println!("{} {} {}", r1, r2, r3); // 完全OK
}
不能 move 被借用的值
前面學過,move 會讓原本的變數不能再用。所以如果一個值還被參考借著,就不能把原本的值 move 走:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let r = &p;
let p2 = p; // 編譯錯誤!p 還被 r 借用中
println!("{:?}", r);
}
r 後面還要使用借來的值,但 let p2 = p; 會把 p move 走,讓 p 從這行之後不能再用。Rust 不允許一邊保留還會被使用的參考,一邊讓原本的變數失效。
不過借用不是從建立參考開始就一路活到大括號結束。當參考最後一次被使用之後,借用就結束了,這時候再 move 就沒問題:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let r = &p;
println!("{:?}", r); // r 最後一次使用
let moved = p; // OK:借用已經結束
println!("{:?}", moved);
}
另一種情況是:你不能從參考後面把非 Copy 的值 move 出來。
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let r = &p;
let moved = *r; // 編譯錯誤!不能從 &Point 後面 move 出 Point
println!("{:?}", moved);
}
*r 是順著參考找到原本的值。Point 沒有實作 Copy,所以把它存進 moved 是 move 而不是複製。
這一條和上面那一條不一樣:上面那個只要等 r 用完就沒事了,這一條不管 r 之後用不用得到都不行。因為 p 才是這個值的擁有者,r 只是借來看的——借來的東西只能看,不能拿走。
當然,如果 *r 後面的值是 i32 這種 Copy 型別,就不一樣了:Rust 會複製一份,而不是 move,因此不違反規則。
懸垂參考(dangling reference)
還有一個重要的規則:參考必須指向還有效的值。如果參考還會被使用,但它指向的值已經失效,就會變成懸垂參考——指向一個已經不存在的地方。Rust 在編譯時期就會阻止這種事情發生。
最常見的懸垂參考,是參考從內層作用域「逃」到外面:
fn main() {
let r;
{
let x = 42;
r = &x; // x 只活在這個大括號裡
} // x 在這裡被丟棄了
println!("{}", r); // 編譯錯誤!r 指向的 x 已經不存在了
}
x 在大括號結束時就被丟棄了,但 r 還試圖在外面使用它——Rust 不允許。
還有一個常見的情況是函數試圖回傳區域變數的參考:
fn bad() -> &i32 {
let x = 42;
&x // x 在函數結束時就被丟棄了,參考會指向一個已經不存在的值
}
fn main() {}
道理是一樣的:x 在函數結束後就消失了,回傳的參考會指向一個不存在的值。
至於 Rust 是怎麼追蹤「參考還有沒有效」的,那就是之後會學到的生命週期(lifetime)概念了。這裡先記住:參考必須指向還有效的值。
範例程式碼
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// 多個不可變借用:OK
let p = Point { x: 1, y: 2 };
let r1 = &p;
let r2 = &p;
println!("r1 = {:?}, r2 = {:?}", r1, r2);
// 一個可變參考:OK
let mut p2 = Point { x: 10, y: 20 };
let r3 = &mut p2;
r3.x += 5;
println!("修改後:{:?}", r3);
// r3 最後一次使用之後,可變借用就結束了,現在可以用 & 借用
let r4 = &p2;
println!("唯讀借用:{:?}", r4);
// 示範:同時多個唯讀借用
let nums = [10, 20, 30, 40, 50];
let slice1 = &nums[0..3];
let slice2 = &nums[2..5];
println!("slice1 = {:?}", slice1);
println!("slice2 = {:?}", slice2);
}
重點整理
- 允許無限制地借用,同樣會讓一份資料出現互相衝突的存取,所以 Rust 制定了借用規則。
- 同一時間只能有一個
&mut,不能同時有兩個可變參考。 &和&mut不能同時存在——要嘛大家都只讀,要嘛只有一個人在改。- 多個
&可以同時存在,多人同時讀沒問題。 - 不能 move 被借用的值:參考之後還會被使用時,不能把原值 move 走;參考最後一次使用後借用就結束,這時候再 move 就沒問題。
- 也不能從參考後面 move 出非
Copy的值:借來的東西只能看,不能拿走,這一條和參考之後用不用得到無關。 - 懸垂參考:參考必須指向還有效的值,不能活得比它指向的值還久——不管是值離開了作用域,還是函數回傳區域變數的參考都不行。
- Rust 靠這些規則在編譯時期就防止存取衝突,之後會學到生命週期來更精確地追蹤參考的有效期。