本文 AI 產出,尚未審核

Rust 課程 – 所有權與借用

主題:所有權(Ownership)概念


簡介

Rust 中,所有權是語言最核心、也是最具革命性的設計之一。它不僅決定了資料在記憶體中的生命週期,還讓編譯器在編譯期就能找出大多數的記憶體安全問題,從而省去執行期的 GC(垃圾回收)開銷。對於從 C/C++、Java、Python 等語言轉過來的開發者來說,所有權的概念最初會感到陌生,但只要掌握了「所有者、借用、作用域」三大法則,就能寫出 安全且效能卓越 的程式。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者建立對所有權的直觀認知,並展示它在真實專案中的應用價值。


核心概念

1. 所有權的三大法則

法則 說明
每個值都有唯一的所有者 變數(或資料結構)在被建立時即取得該值的所有權。
所有權在同一時間只能有一個 當所有權被「移動」(move) 到另一個變數時,原變數失去對該值的存取權。
所有權離開作用域時自動釋放 變數離開其作用域({})時,Rust 會自動呼叫 drop 釋放資源,無需手動 free

這三條規則讓 Rust 能在 編譯期 完成記憶體安全檢查,避免「使用已釋放的記憶體」或「雙重釋放」等常見錯誤。

2. 移動(Move)與深拷貝(Clone)

  • Move:將值的所有權從 A 變數搬到 B 變數,A 變數變成「未初始化」狀態,若再使用會編譯錯誤。
  • Clone:對於實作了 Clone trait 的類型,可顯式呼叫 clone() 產生深拷貝,兩個變數各自擁有獨立的所有權。

Tip:對於大多數堆疊資料(如 i32bool)會自動執行「Copy」語義,這表示在賦值時會 複製 而不是 移動,不會失去所有權。

3. 借用(Borrow)

所有權可以暫時「借」給其他程式碼使用,分為 不可變借用&T)與 可變借用&mut T)。

  • 不可變借用:同時允許多個,只要沒有可變借用,就可以安全讀取。
  • 可變借用:在同一時間只能有 一個,且不能與任何不可變借用同時存在。

借用的生命週期由編譯器透過 借用檢查器(borrow checker) 追蹤,確保不會出現資料競爭(data race)或懸空指標(dangling reference)。


程式碼範例

以下範例使用 Rustrust 標記),每段程式碼皆附上說明註解,方便讀者快速掌握概念。

範例 1:最簡單的所有權轉移

fn main() {
    let s1 = String::from("Hello Rust"); // s1 成為字串的所有者
    let s2 = s1;                         // 所有權被移動到 s2,s1 失效

    // println!("{}", s1); // 編譯錯誤:s1 已被移走
    println!("{}", s2);   // 正常輸出
}

重點String 是堆疊上只保存指標、長度、容量的結構,真正的字元資料在堆上。移動時只搬移指標,不會複製整段字串,故效率高。

範例 2:使用 clone() 產生深拷貝

fn main() {
    let s1 = String::from("clone me");
    let s2 = s1.clone(); // 產生一個全新的字串,兩者互不影響

    println!("s1 = {}", s1); // 仍然可以使用
    println!("s2 = {}", s2);
}

說明clone() 會在堆上重新分配記憶體,拷貝內容。對於大型資料結構,頻繁 clone 可能造成效能問題,應視需求選擇。

範例 3:不可變借用與多重讀取

fn main() {
    let data = vec![1, 2, 3, 4];

    // 同時取得兩個不可變參考
    let r1 = &data;
    let r2 = &data;

    println!("r1: {:?}, r2: {:?}", r1, r2);
    // data 仍然可以在此處被讀取
    println!("data length = {}", data.len());
}

要點:只要是不可變借用,Rust 允許 任意多個 同時存在,且原始變數仍可讀取。

範例 4:可變借用的唯一性

fn main() {
    let mut numbers = vec![10, 20, 30];

    {
        let mut_ref = &mut numbers; // 可變借用開始
        mut_ref.push(40);
        // 此區塊內,不能再取得其他借用
    } // mutable_ref 在此離開作用域,借用結束

    // 重新取得不可變借用,安全讀取
    let read_ref = &numbers;
    println!("numbers: {:?}", read_ref);
}

說明:可變借用必須在同一時間內保持唯一,否則編譯器會報錯。借用的作用域結束後,所有權恢復,可再取得其他借用。

範例 5:函式參數的所有權與借用

fn takes_ownership(s: String) {
    // s 成為此函式的所有者,離開作用域後自動釋放
    println!("Got: {}", s);
}

fn borrows_immutably(s: &String) {
    // 只借用,不取得所有權
    println!("Read only: {}", s);
}

fn main() {
    let text = String::from("function demo");

    takes_ownership(text); // 所有權被移走,text 失效
    // println!("{}", text); // 編譯錯誤

    let another = String::from("borrow demo");
    borrows_immutably(&another); // 只借用
    println!("still usable: {}", another); // 仍可使用
}

關鍵:在設計 API 時,決定是「取得所有權」還是「借用」會直接影響呼叫端的使用彈性與效能。


常見陷阱與最佳實踐

陷阱 說明 最佳實踐
使用已移走的變數 移動後仍嘗試存取會編譯錯誤。 在需要保留原值時,使用 clone() 或改為借用 (&) 。
同時持有可變與不可變借用 會觸發借用檢查器錯誤。 把可變借用的作用域縮小,或使用 內部可變性RefCell<T>Mutex<T>)在需要動態檢查時。
過度使用 clone() 產生不必要的深拷貝,浪費記憶體與 CPU。 盡量透過借用或 Rc<T>/Arc<T> 共享所有權,僅在必須擁有獨立所有權時才 clone。
忘記 mut 關鍵字 想要修改資料卻忘記宣告為可變,導致編譯錯誤。 在需要可變借用或可變變數時,務必加上 mut,並遵守唯一可變借用規則。
遞迴結構的所有權 直接使用 Box<T> 可能導致循環引用,記憶體無法釋放。 使用 Rc<T> + Weak<T>(或 Arc<T> + Weak<T>)打破循環,讓記憶體正確回收。

額外建議

  1. 盡量讓變數在最小作用域內活躍,減少借用檢查器的限制。
  2. 利用 #[derive(Debug)] 為自訂結構加上除錯輸出,方便觀察所有權搬移的時機。
  3. 閱讀編譯器錯誤訊息,Rust 的錯誤訊息非常友善,往往直接告訴你是哪一條所有權規則被違反。

實際應用場景

1. 高效能網路服務

在使用 tokioasync-std 實作非阻塞伺服器時,所有權 能確保每個請求的資料在處理完畢後自動釋放,避免記憶體泄漏。範例:

async fn handle(conn: TcpStream) {
    let mut buf = vec![0u8; 1024];
    let n = conn.read(&mut buf).await.unwrap();
    // `buf` 的所有權仍在此函式,離開後自動釋放
    process(&buf[..n]).await;
}

buf 只在 handle 內部存在,結束即釋放,省去手動 free 的麻煩。

2. 嵌入式系統

在資源受限的 MCU 上,不需要 GC 的 Rust 能提供 零成本抽象。所有權保證了堆疊資料在離開作用域時即被回收,減少動態配置需求。

fn blink(mut led: Pin<Output>) {
    for _ in 0..5 {
        led.set_high().unwrap();
        delay_ms(200);
        led.set_low().unwrap();
        delay_ms(200);
    } // led 在此被 drop,釋放 GPIO 資源
}

3. 多執行緒資料共享

使用 Arc<T>(Atomic Reference Counted)結合 Mutex<T>,在保留 所有權共享 的同時,仍能透過 借用檢查 防止資料競爭:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let shared_vec = Arc::new(Mutex::new(vec![1, 2, 3]));

    let mut handles = vec![];
    for _ in 0..4 {
        let vec_clone = Arc::clone(&shared_vec);
        handles.push(thread::spawn(move || {
            let mut guard = vec_clone.lock().unwrap(); // 可變借用
            guard.push(4);
        }));
    }

    for h in handles { h.join().unwrap(); }
    println!("Result: {:?}", *shared_vec.lock().unwrap());
}

此模式在 Web 伺服器、資料庫緩衝 等需要跨執行緒共享狀態的場景相當常見。


總結

  • 所有權 是 Rust 保障記憶體安全與效能的基石,透過「唯一所有者」與「作用域自動釋放」兩大原則,讓開發者免除手動管理記憶體的負擔。
  • 移動 (move)深拷貝 (clone) 讓我們在「共享」與「獨佔」之間取得平衡;借用 (borrow) 則提供了安全的暫時存取方式。
  • 熟練 不可變/可變借用 的規則,能避免大多數的競爭條件與懸空指標問題。
  • 在實務開發中,正確運用所有權與借用可以大幅提升 效能、可靠性與可維護性,尤其在高併發網路服務、嵌入式系統與多執行緒應用中更是不可或缺。

掌握了所有權的概念後,你就能在 Rust 的安全模型上自由發揮,寫出既快速又安全的程式碼。祝你在 Rust 的旅程中玩得開心、寫得順手! 🚀