借用規則
本集目標
理解 Rust 的借用規則:同時只能有一個 &mut 或多個 &,以及懸垂參考的問題。
概念說明
為什麼需要規則?
上一集我們學了 &mut 可變借用。但如果 Rust 允許你同時有多個可變借用,會怎樣?
想像你有一串鑰匙圈。借給很多人看(&)沒問題——大家都只是看,不會改變鑰匙圈上有什麼。但如果同時借給兩個人修改(&mut)——A 在加一把新鑰匙,B 同時在拆掉那把——結果就不可預測了。
這也是資料競爭(data race),會導致各種奇怪的 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
}
懸垂參考(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不能同時存在——要嘛大家都只讀,要嘛只有一個人在改- 多個
&可以同時存在,多人同時讀沒問題 - 懸垂參考:參考不能活得比它指向的值還久——不管是值離開了作用域,還是函數回傳區域變數的參考都不行
- Rust 靠這些規則在編譯時就防止資料競爭,之後會學到生命週期來更精確地追蹤參考的有效期