Rust 課程:所有權與借用 ── 可變與不可變參考
簡介
在 Rust 中,所有權 (ownership) 與 借用 (borrowing) 是語言安全性的核心機制。它們讓編譯器在編譯階段就能捕捉到大多數記憶體錯誤,從而保證程式在執行時不會發生資料競爭或懸空指標。
在所有權與借用的概念裡,最常見的操作就是取得 不可變參考 (&T) 或 可變參考 (&mut T)。這兩種參考的差異不僅影響程式的可讀性與可維護性,更直接關係到執行緒安全與效能。
本篇文章將以 淺顯易懂 的方式說明什麼是可變與不可變參考、它們的使用規則、常見的陷阱以及在實務開發中的應用場景,幫助初學者快速上手,同時也提供給已有基礎的開發者作為最佳實踐的參考。
核心概念
1. 為什麼需要參考?
在 C/C++ 中,我們常用指標直接操作記憶體,但這會帶來 空指標、野指標、資料競爭 等問題。Rust 用 參考 取代裸指標,並在編譯期強制以下規則:
| 規則 | 說明 |
|---|---|
| 同時只能有一個可變參考 | 防止同時寫入造成資料競爭 |
| 不可變參考可以有多個 | 只讀不會改變資料,安全共享 |
| 不可變參考與可變參考不可同時存在 | 防止讀寫衝突 |
只要遵守這兩條規則,Rust 保證程式在執行時不會出現資料競爭或懸空指標。
2. 不可變參考 (&T)
不可變參考提供 唯讀 的視圖,允許多個程式碼同時借用同一個資料。
基本語法
fn print_len(s: &String) {
// 只能讀取,不能修改
println!("長度是 {}", s.len());
}
&String表示「借用一個String的不可變參考」- 函式內部只能呼叫
String的 只讀方法(例如len、as_str)
範例 1:多個不可變參考同時存在
fn main() {
let text = String::from("Rust 程式語言");
let r1 = &text; // 第一個不可變參考
let r2 = &text; // 第二個不可變參考
println!("r1: {}, r2: {}", r1, r2);
// 此時仍然可以使用 text 本身,只要不取得可變參考
println!("原始文字: {}", text);
}
重點:只要沒有取得
&mut,text可以同時被多個&參考。
3. 可變參考 (&mut T)
可變參考允許 修改 被借用的資料,但同一時間只能有 唯一 的可變參考,且不可與任何不可變參考同時存在。
基本語法
fn append_exclamation(s: &mut String) {
s.push('!'); // 可以修改
}
&mut String表示「借用一個String的可變參考」- 函式內部可以呼叫 可變方法(例如
push、clear)
範例 2:唯一的可變參考
fn main() {
let mut data = vec![1, 2, 3];
let r = &mut data; // 取得唯一的可變參考
r.push(4);
println!("更新後: {:?}", r);
// 此時 data 已被借用,不能再取得其他參考
// println!("{:?}", data); // 編譯錯誤
}
提醒:
data必須是mut,否則無法取得&mut。
4. 同時取得不可變與可變參考的錯誤範例
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可變參考
let r2 = &mut s; // 想要同時取得可變參考 ❌
r2.push_str(", world");
println!("r1: {}, r2: {}", r1, r2);
}
編譯器會報錯:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:5:14
|
4 | let r1 = &s; // 不可變參考
| -- immutable borrow occurs here
5 | let r2 = &mut s; // 想要同時取得可變參考 ❌
| ^^^^^^^ mutable borrow occurs here
6 | r2.push_str(", world");
7 | println!("r1: {}, r2: {}", r1, r2);
| -- immutable borrow later used here
這正是 所有權與借用規則 在編譯期的保護。
5. 作用域與借用的結束時機
借用的生命週期(lifetime)由 作用域 決定。只要參考仍在使用中,原始值就不能被重新借用或移動。
範例 3:借用在較小的作用域內結束
fn main() {
let mut v = vec![10, 20, 30];
{
let slice = &mut v[0..2]; // 可變切片只在此區塊有效
slice[0] = 99;
println!("slice: {:?}", slice);
} // slice 在此離開作用域,借用結束
// 重新取得不可變參考已合法
let r = &v;
println!("v after mutation: {:?}", r);
}
技巧:將可變借用限制在最小的區塊內,可減少與其他程式碼的衝突,提升可讀性與安全性。
6. 內部可變性 (Interior Mutability)
有時候我們需要在「不可變」的結構裡仍能修改資料,Rust 提供 RefCell<T>、Mutex<T> 等 內部可變性 型別。這些型別在編譯期不保證唯一性,而是在執行時檢查。
範例 4:使用 RefCell 允許暫時的可變借用
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 取得不可變參考,但仍可在執行時取得可變借用
*data.borrow_mut() += 1;
println!("結果: {}", data.borrow());
}
注意:
RefCell只在單執行緒環境安全;跨執行緒則使用Mutex或RwLock。
7. 何時使用 & vs &mut
| 情境 | 建議使用 |
|---|---|
| 只需要讀取資料 | &T |
| 需要修改資料且沒有其他同時讀取需求 | &mut T |
| 多執行緒共享且只讀 | Arc<T> + &T |
| 多執行緒共享且需要寫入 | Arc<Mutex<T>> 或 Arc<RwLock<T>> |
| 想在不可變結構內部改變狀態(例如緩存) | RefCell<T> / Cell<T> |
常見陷阱與最佳實踐
1. 忘記 mut 會導致無法取得 &mut
let s = String::from("hello"); // 沒有 mut
let r = &mut s; // 編譯錯誤
解法:在宣告時加上 mut,或重新思考是否真的需要可變參考。
2. 過度使用 RefCell 逃離編譯期檢查
RefCell 讓程式在執行時才檢查借用規則,若濫用會產生 panic。
最佳實踐:盡量在編譯期解決借用衝突,只有在真的需要「在不可變結構內部改變」時才使用 RefCell。
3. 在迭代時同時取得可變參考
for i in &mut vec {
// 同時取得 vec 的可變參考與迭代器的不可變參考,會衝突
}
解法:使用 for i in vec.iter_mut(),或先把迭代結果收集到暫存變數。
4. 借用的生命週期比預期長
有時候編譯器會因為「隱式的借用」而延長生命週期,導致無法在同一作用域內重新借用。
技巧:使用大括號明確縮小借用範圍,或使用 drop() 提前釋放。
5. 在函式返回參考時忽略生命週期
fn first_word(s: &String) -> &str { // 錯誤,缺少生命週期標註
&s[0..1]
}
解法:加入生命週期標註 fn first_word<'a>(s: &'a String) -> &'a str,或改用 String 所有權返回。
實際應用場景
1. 資料結構的內部修改
在實作 鏈結串列、樹 等資料結構時,節點往往需要在外部不可變的情況下被修改。此時可以使用 RefCell 包裝子節點,讓演算法在遍歷時仍能安全地改變指向。
2. 多執行緒的共享狀態
Web 伺服器或遊戲伺服器常需要共享全域設定或緩存。使用 Arc<Mutex<T>> 或 Arc<RwLock<T>>,讓多個執行緒同時取得 不可變參考(讀取)或 可變參考(寫入)而不會產生資料競爭。
3. 函式庫 API 設計
公開函式庫時,若函式只需要讀取參數,請使用 &T;若需要修改,則使用 &mut T。這樣的設計能讓使用者在編譯期即知道是否會改變傳入的資料,提升 API 的可預測性。
4. 效能優化
在大量資料處理(例如圖像處理、數值計算)時,避免不必要的所有權搬移(move)或資料複製(clone),改以 借用 的方式傳遞資料。這不僅降低記憶體開銷,也讓程式更具可讀性。
總結
- 不可變參考 (
&T) 允許多個同時存在的唯讀借用,是最安全、最常用的方式。 - 可變參考 (
&mut T) 必須唯一,且在同一時間不能與任何不可變參考共存,確保寫入操作不會與讀取衝突。 - 借用的生命週期 由作用域決定,適時使用大括號或
drop()可以縮短借用時間,避免不必要的衝突。 - 內部可變性(
RefCell、Mutex)提供了在編譯期無法滿足的彈性,但使用時需謹慎,以免失去編譯期保證。 - 在設計 API、資料結構或多執行緒程式時,正確選擇
&、&mut、Arc、Mutex等工具,能讓程式既安全又高效。
掌握了 可變與不可變參考 的使用規則後,你就能在 Rust 中寫出「零資料競爭」的程式,同時享受到編譯器提供的強大保護。祝你在 Rust 的所有權與借用之路上越走越遠,寫出更安全、更快的程式!