本文 AI 產出,尚未審核

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),確保以下兩條規則不被違反:

  1. 同時只能有一個可變參考,或 任意數量的不可變參考
  2. 參考的生命週期(Lifetime) 必須不超過被參考值的生命週期。

違反任一規則,編譯器會直接報錯,避免了執行時的資料競爭與未定義行為。

3. 生命週期(Lifetimes)簡介

生命週期是編譯器用來追蹤參考有效期間的概念。大多數情況下,編譯器能自行推斷生命週期;但在函式間傳遞參考時,可能需要手動標註:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

上例中的 'a 表示 ab 與返回值必須共用同一個最短的生命週期,保證返回的參考在使用時仍然有效。

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 確保 所有權參考 在相同或較短的作用域;必要時使用 StringVecclone
忘記解引用 直接對 &mut T 使用方法會失敗 使用 * 解引用或直接呼叫 value.method()(自動解引用)
過度使用 clone 失去零成本抽象的優勢 只在真的需要所有權轉移且無法透過借用解決時才 clone
忽視可變參考的唯一性 多個 &mut 造成資料競爭 使用 內部可變性RefCellMutex)在需要多重可變存取的情境下

最佳實踐

  1. 盡量使用不可變參考:除非必須修改,保持資料的不可變性可提升程式的可讀性與安全性。
  2. 縮小借用範圍:將借用限制在最小的程式區塊內,減少與其他借用衝突的機會。
  3. 利用 split_at_mutchunks_mut 等 API:在需要同時操作同一容器的不同區段時,使用標準函式取得不重疊的可變參考。
  4. 適時使用 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)與 內部可變性RefCellRwLock)解決更複雜的需求。

只要遵循上述原則,你就能在日常開發、效能敏感的系統、甚至多執行緒環境中,安全且高效地運用 Rust 的借用機制。祝你在 Rust 的旅程中,寫出更安全、更快的程式!