Rust 課程 – 所有權與借用
主題:所有權(Ownership)概念
簡介
在 Rust 中,所有權是語言最核心、也是最具革命性的設計之一。它不僅決定了資料在記憶體中的生命週期,還讓編譯器在編譯期就能找出大多數的記憶體安全問題,從而省去執行期的 GC(垃圾回收)開銷。對於從 C/C++、Java、Python 等語言轉過來的開發者來說,所有權的概念最初會感到陌生,但只要掌握了「所有者、借用、作用域」三大法則,就能寫出 安全且效能卓越 的程式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者建立對所有權的直觀認知,並展示它在真實專案中的應用價值。
核心概念
1. 所有權的三大法則
| 法則 | 說明 |
|---|---|
| 每個值都有唯一的所有者 | 變數(或資料結構)在被建立時即取得該值的所有權。 |
| 所有權在同一時間只能有一個 | 當所有權被「移動」(move) 到另一個變數時,原變數失去對該值的存取權。 |
| 所有權離開作用域時自動釋放 | 變數離開其作用域({})時,Rust 會自動呼叫 drop 釋放資源,無需手動 free。 |
這三條規則讓 Rust 能在 編譯期 完成記憶體安全檢查,避免「使用已釋放的記憶體」或「雙重釋放」等常見錯誤。
2. 移動(Move)與深拷貝(Clone)
- Move:將值的所有權從 A 變數搬到 B 變數,A 變數變成「未初始化」狀態,若再使用會編譯錯誤。
- Clone:對於實作了
Clonetrait 的類型,可顯式呼叫clone()產生深拷貝,兩個變數各自擁有獨立的所有權。
Tip:對於大多數堆疊資料(如
i32、bool)會自動執行「Copy」語義,這表示在賦值時會 複製 而不是 移動,不會失去所有權。
3. 借用(Borrow)
所有權可以暫時「借」給其他程式碼使用,分為 不可變借用(&T)與 可變借用(&mut T)。
- 不可變借用:同時允許多個,只要沒有可變借用,就可以安全讀取。
- 可變借用:在同一時間只能有 一個,且不能與任何不可變借用同時存在。
借用的生命週期由編譯器透過 借用檢查器(borrow checker) 追蹤,確保不會出現資料競爭(data race)或懸空指標(dangling reference)。
程式碼範例
以下範例使用 Rust(rust 標記),每段程式碼皆附上說明註解,方便讀者快速掌握概念。
範例 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>)打破循環,讓記憶體正確回收。 |
額外建議:
- 盡量讓變數在最小作用域內活躍,減少借用檢查器的限制。
- 利用
#[derive(Debug)]為自訂結構加上除錯輸出,方便觀察所有權搬移的時機。 - 閱讀編譯器錯誤訊息,Rust 的錯誤訊息非常友善,往往直接告訴你是哪一條所有權規則被違反。
實際應用場景
1. 高效能網路服務
在使用 tokio 或 async-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 的旅程中玩得開心、寫得順手! 🚀