Rust 課程 – 所有權與借用
主題:借用(Borrowing)與參考(References)
簡介
在 Rust 中,所有權(Ownership) 是語言安全與效能的核心機制,而 借用(Borrowing) 則是所有權的延伸與補充。透過借用,我們可以在不取得所有權的前提下暫時使用資料,讓多個程式碼區塊安全地共享同一個值,而不會產生資料競爭或記憶體錯誤。
對於剛接觸 Rust 的開發者而言,借用概念往往是最具挑戰性的部分;但一旦掌握,就能寫出 零成本抽象、避免資料競爭 的程式,同時享受到編譯期的錯誤檢查。本文將以淺顯易懂的方式說明借用與參考的運作原理,並提供實用範例、常見陷阱與最佳實踐,幫助你在日常開發中正確運用這項功能。
核心概念
1. 什麼是「參考」?
在 Rust 中,參考(Reference) 是一個指向某個值的指標,但它不擁有該值的所有權。參考的語法使用 &(不可變參考)或 &mut(可變參考):
let x = 10; // x 擁有所有權
let r = &x; // r 是不可變參考,指向 x
let mut y = 20;
let r_mut = &mut y; // r_mut 是可變參考,指向 y
- 不可變參考 (
&T):只能讀取資料,不能修改。多個不可變參考可以同時存在。 - 可變參考 (
&mut T):允許修改資料,但在同一時間只能有 一個 可變參考,且不可與任何不可變參考同時存在。
2. 借用規則(Borrow Checker)
Rust 的編譯器會在編譯期執行 借用檢查(Borrow Checker),確保以下兩條規則不被違反:
- 同時只能有一個可變參考,或 任意數量的不可變參考。
- 參考的生命週期(Lifetime) 必須不超過被參考值的生命週期。
違反任一規則,編譯器會直接報錯,避免了執行時的資料競爭與未定義行為。
3. 生命週期(Lifetimes)簡介
生命週期是編譯器用來追蹤參考有效期間的概念。大多數情況下,編譯器能自行推斷生命週期;但在函式間傳遞參考時,可能需要手動標註:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
上例中的 'a 表示 a、b 與返回值必須共用同一個最短的生命週期,保證返回的參考在使用時仍然有效。
4. 借用的常見模式
| 模式 | 說明 | 範例 |
|---|---|---|
| 只讀借用 | 多個不可變參考同時存取資料 | let r1 = &v; let r2 = &v; |
| 可寫借用 | 單一可變參考取得寫入權限 | let r_mut = &mut v; *r_mut += 1; |
| 分割借用 | 同時取得結構體的不同欄位的可變參考(使用 split_at_mut) |
let (left, right) = slice.split_at_mut(mid); |
| 暫時借用 | 使用作用域限制借用時間,避免長時間佔用 | { let r = &v; println!("{}", r); } |
程式碼範例
範例 1:不可變參考的安全共享
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// 同時取得多個不可變參考,編譯器允許
let r1 = &numbers;
let r2 = &numbers;
println!("第一個參考的長度: {}", r1.len());
println!("第二個參考的第一個元素: {}", r2[0]);
// numbers 本身仍保有所有權,可以在此之後使用
println!("原始向量: {:?}", numbers);
}
重點:只要是不可變參考,任意數量都可以同時存在,且不會影響原始資料的所有權。
範例 2:可變參考的唯一性
fn increment(value: &mut i32) {
*value += 1; // 解引用後修改
}
fn main() {
let mut count = 0;
// 取得唯一的可變參考
let r_mut = &mut count;
increment(r_mut); // 傳遞給函式
// 此時 r_mut 仍然有效,不能再取得另一個可變參考
println!("count = {}", count);
}
注意:若在
increment之外再嘗試取得&mut count,編譯器會報錯,因為已經有一個活躍的可變參考。
範例 3:同時擁有不可變與可變參考(錯誤示範)
fn main() {
let mut data = vec![10, 20, 30];
let r1 = &data; // 不可變參考
let r_mut = &mut data; // 可變參考 ← 這裡會編譯錯誤
println!("{:?}", r1);
r_mut.push(40);
}
編譯錯誤訊息
error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable --> src/main.rs:6:20 | 4 | let r1 = &data; // 不可變參考 | ---- immutable borrow occurs here 5 | let r_mut = &mut data; // 可變參考 ← 這裡會編譯錯誤 | ^^^^^ mutable borrow occurs here 6 | println!("{:?}", r1); | -- immutable borrow later used here
解法:在取得可變參考前,先讓不可變參考離開作用域(使用大括號或
drop):
{
let r1 = &data;
println!("{:?}", r1);
} // r1 在此離開作用域
let r_mut = &mut data;
r_mut.push(40);
範例 4:切片的分割借用(split_at_mut)
fn main() {
let mut scores = [10, 20, 30, 40, 50];
// 同時取得左半部與右半部的可變參考
let (left, right) = scores.split_at_mut(3);
// 左半部每個元素加 1,右半部每個元素減 1
for v in left.iter_mut() {
*v += 1;
}
for v in right.iter_mut() {
*v -= 1;
}
println!("更新後的陣列: {:?}", scores);
}
說明:
split_at_mut內部使用 安全的內部可變性(interior mutability),確保左、右兩段的參考在編譯期不會重疊。
範例 5:生命週期標註的實際需求
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("Hello");
let result;
{
let string2 = String::from("World!!!");
result = longest(&string1, &string2);
// 此時 result 的生命週期仍受 string2 的限制
println!("較長的字串: {}", result);
} // string2 離開作用域,result 不能再使用
}
關鍵:如果嘗試在
string2離開後使用result,編譯器會阻止,因為result的生命週期被限定在string2內。
常見陷阱與最佳實踐
| 陷阱 | 可能的錯誤 | 解決方案或最佳實踐 |
|---|---|---|
| 同時持有不可變與可變參考 | 編譯錯誤 cannot borrow as mutable because it is also borrowed as immutable |
使用 作用域(大括號)或 std::mem::drop 讓不可變參考先離開 |
| 參考活得比原值久 | 生命週期錯誤 borrowed value does not live long enough |
確保 所有權 與 參考 在相同或較短的作用域;必要時使用 String 或 Vec 的 clone |
| 忘記解引用 | 直接對 &mut T 使用方法會失敗 |
使用 * 解引用或直接呼叫 value.method()(自動解引用) |
過度使用 clone |
失去零成本抽象的優勢 | 只在真的需要所有權轉移且無法透過借用解決時才 clone |
| 忽視可變參考的唯一性 | 多個 &mut 造成資料競爭 |
使用 內部可變性(RefCell、Mutex)在需要多重可變存取的情境下 |
最佳實踐
- 盡量使用不可變參考:除非必須修改,保持資料的不可變性可提升程式的可讀性與安全性。
- 縮小借用範圍:將借用限制在最小的程式區塊內,減少與其他借用衝突的機會。
- 利用
split_at_mut、chunks_mut等 API:在需要同時操作同一容器的不同區段時,使用標準函式取得不重疊的可變參考。 - 適時使用
RefCell/Rc:在需要多所有權或在執行期檢查可變性時,考慮RefCell<T>(單執行緒)或RwLock<T>(多執行緒)等內部可變性工具。
實際應用場景
1. 資料處理管線(Streaming)
在高效能的資料流處理(如 CSV 解析、網路封包)中,我們往往需要 在同一緩衝區上多次讀寫。使用不可變參考讀取資料、使用可變參考寫入結果,配合 split_at_mut 讓不同階段同時存取不同區段,可避免不必要的記憶體複製。
fn process_chunk(chunk: &mut [u8]) {
let (header, body) = chunk.split_at_mut(8);
// 讀取 header(不可變)
let version = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
// 修改 body(可變)
for b in body.iter_mut() {
*b = b.wrapping_add(version as u8);
}
}
2. GUI 或遊戲引擎的實體系統
在 Entity‑Component‑System(ECS)架構中,系統往往需要 同時讀取多個組件,但只能 寫入少數組件。透過借用規則,我們可以在編譯期保證不會同時寫入同一組件,避免競爭條件。
fn movement_system(positions: &mut [Position], velocities: &[Velocity]) {
for (pos, vel) in positions.iter_mut().zip(velocities.iter()) {
pos.x += vel.dx;
pos.y += vel.dy;
}
}
3. 多執行緒共享資料
Rust 的 Arc<T>(原子參考計數)結合 RwLock<T>,讓多執行緒可以 同時讀取(多個不可變參考)或 唯一寫入(單一可變參考)。借用檢查在單執行緒階段仍然適用,確保即使在 RwLock 內部也不會同時取得兩個可變參考。
use std::sync::{Arc, RwLock};
use std::thread;
let shared = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
for _ in 0..4 {
let cloned = Arc::clone(&shared);
handles.push(thread::spawn(move || {
// 只讀取
let data = cloned.read().unwrap();
println!("讀取: {:?}", *data);
}));
}
// 寫入
{
let mut data = shared.write().unwrap();
data.push(4);
}
總結
借用與參考是 Rust 安全、零成本抽象 的關鍵。透過 不可變參考的多重共享、可變參考的唯一性,以及 編譯期的生命週期檢查,我們可以在不犧牲效能的前提下,寫出 避免資料競爭、易於維護 的程式碼。
- 掌握借用規則:同時只能有一個可變參考,或任意數量的不可變參考。
- 適時縮小借用範圍:使用區塊或
drop釋放參考,降低衝突機會。 - 善用標準函式(如
split_at_mut)與 內部可變性(RefCell、RwLock)解決更複雜的需求。
只要遵循上述原則,你就能在日常開發、效能敏感的系統、甚至多執行緒環境中,安全且高效地運用 Rust 的借用機制。祝你在 Rust 的旅程中,寫出更安全、更快的程式!