本文 AI 產出,尚未審核

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只讀方法(例如 lenas_str

範例 1:多個不可變參考同時存在

fn main() {
    let text = String::from("Rust 程式語言");
    let r1 = &text; // 第一個不可變參考
    let r2 = &text; // 第二個不可變參考
    println!("r1: {}, r2: {}", r1, r2);
    // 此時仍然可以使用 text 本身,只要不取得可變參考
    println!("原始文字: {}", text);
}

重點:只要沒有取得 &muttext 可以同時被多個 & 參考。


3. 可變參考 (&mut T)

可變參考允許 修改 被借用的資料,但同一時間只能有 唯一 的可變參考,且不可與任何不可變參考同時存在。

基本語法

fn append_exclamation(s: &mut String) {
    s.push('!'); // 可以修改
}
  • &mut String 表示「借用一個 String 的可變參考」
  • 函式內部可以呼叫 可變方法(例如 pushclear

範例 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 只在單執行緒環境安全;跨執行緒則使用 MutexRwLock


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() 可以縮短借用時間,避免不必要的衝突。
  • 內部可變性RefCellMutex)提供了在編譯期無法滿足的彈性,但使用時需謹慎,以免失去編譯期保證。
  • 在設計 API、資料結構或多執行緒程式時,正確選擇 &&mutArcMutex 等工具,能讓程式既安全又高效。

掌握了 可變與不可變參考 的使用規則後,你就能在 Rust 中寫出「零資料競爭」的程式,同時享受到編譯器提供的強大保護。祝你在 Rust 的所有權與借用之路上越走越遠,寫出更安全、更快的程式!