上一篇結尾留了一個伏筆:String 與 &str 為什麼有「擁有」與「借用」之分。這一篇就是 Rust 最招牌、最讓初學者卡關,但也最值得花時間搞懂的一塊——所有權(ownership)系統。把這套規則裝進腦子,後面寫 struct、Vec、字串處理、錯誤處理才會順。
為什麼需要所有權
記憶體管理三條路:
- 手動管理(C / C++):自己
malloc/free,效能最好但容易出錯(懸空指標、雙重釋放、記憶體洩漏) - 垃圾回收(GC)(Java / Go / Python):runtime 定期掃描並回收,安全但有 runtime 成本與停頓
- 所有權系統(Rust):編譯期靜態分析,沒有 GC、沒有 runtime 開銷,但要遵守一套規則
Rust 選第三條,代價是要把所有權規則放進腦子裡。
三條核心規則
- 每個值都有一個所有者(owner)
- 同一時間只能有一個所有者
- 所有者離開 scope 時,值會被 drop(釋放)
Stack vs Heap 的差別
理解所有權前要先有這層基礎。
| Stack | Heap | |
|---|---|---|
| 大小 | 編譯期已知 | 執行期才知道 |
| 速度 | 快(push / pop) | 較慢(要找空間) |
| 例子 | i32、bool、char、固定大小 array | String、Vec<T>、Box<T> |
純量型別都在 stack 上,複製成本極低,所以實作了 Copy trait——賦值就直接複製,不牽涉所有權移轉。
let a = 5;
let b = a; // 直接複製 stack 上的 4 byte
println!("{a} {b}"); // ✅ a 還能用
String 不一樣。它在 stack 上放三個欄位:ptr(指向 heap 字元資料的起點)、len(目前長度)、cap(已配置容量),實際字元資料在 heap 上:
stack heap
┌──────────┐ ┌─────────────┐
│ ptr │───────→│ h e l l o │
│ len = 5│ └─────────────┘
│ cap = 5│
└──────────┘
move 時只複製 stack 上這三個欄位,heap 上的資料本身不動。
Move:所有權的轉移
let s1 = String::from("hello");
let s2 = s1; // 所有權從 s1 移轉到 s2
println!("{s1}"); // ❌ 編譯錯誤:value borrowed here after move
發生了什麼:
s1建立後擁有那塊 heap 記憶體let s2 = s1不是深拷貝,也不是淺拷貝(不會留下兩個指向同一塊 heap 的指標)- Rust 把
s1的 stack 部分(ptr/len/cap)複製給s2,然後讓s1失效 - 之後只有
s2是合法的所有者,s1不能再用
這個設計避免了「兩個變數都覺得自己擁有同一塊記憶體」造成的雙重釋放。
move 之前 move 之後
s1 ──┐ s1 ✗(失效)
├─→ heap "hello" s2 ──→ heap "hello"
要真正深拷貝,明確呼叫 .clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷貝,heap 上多開一份
println!("{s1} {s2}"); // ✅ 兩個都能用
看到 .clone() 就要意識到:這裡有一次堆積分配。對效能敏感的場景別亂 clone。
Copy trait:哪些型別不會 move
實作了 Copy trait 的型別,賦值時走複製、不走 move:
- 所有整數、浮點、
bool、char - 只包含
Copy型別的 tuple,例如(i32, i32) - 固定大小 array,元素是
Copy
String、Vec<T> 這類持有 heap 資源的型別沒有 Copy——複製它們要做堆積分配,Rust 要求你明確寫出 .clone()。
函式呼叫也會 move
fn take(s: String) {
println!("{s}");
} // s 在這裡離開 scope,被 drop
fn main() {
let s = String::from("hi");
take(s);
println!("{s}"); // ❌ s 已經被 move 進 take,drop 掉了
}
每次呼叫函式都要把所有權交出去,太麻煩。解法是借用(borrowing)。
借用:&T 與 &mut T
借用就是「我只是看一下,所有權還是你的」。
fn read(s: &String) {
println!("{s}");
} // s 是借用,離開 scope 不會 drop 原始字串
fn main() {
let s = String::from("hi");
read(&s); // 傳的是參考,不是所有權
println!("{s}"); // ✅ s 還活著
}
&s 會建立一個指向 s 的不可變參考,函式那端接到的型別就是 &String。
要在借用期間修改值,就用可變借用 &mut:
fn append(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
append(&mut s);
println!("{s}"); // hello world
}
注意:要用 &mut 借出,原變數本身必須是 mut。
借用規則:核心限制
在任何時間點,對同一個資料:
- 可以有任意多個不可變借用(
&T),或 - 只能有一個可變借用(
&mut T)
兩者不能同時存在。
let mut s = String::from("hi");
let r1 = &s;
let r2 = &s; // ✅ 多個不可變借用 OK
println!("{r1} {r2}");
let r3 = &mut s; // ✅ 此時 r1、r2 已不再使用
r3.push_str("!");
println!("{r3}");
違規範例:
let mut s = String::from("hi");
let r1 = &s;
let r2 = &mut s; // ❌ 已有不可變借用,不能再借可變
println!("{r1} {r2}");
let mut v = vec![1, 2, 3];
let a = &mut v;
let b = &mut v; // ❌ 同時兩個可變借用
a.push(4);
b.push(5);
這條規則防的是資料競爭(data race)——在編譯期就把多執行緒問題的根源之一擋掉。
NLL:借用何時結束
Rust 用「非詞法生命週期(Non-Lexical Lifetimes, NLL)」判斷借用何時結束——借用的有效範圍只到最後一次使用的那一行,不是整個 scope。
let mut s = String::from("hi");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}"); // r1、r2 在這之後不再使用
let r3 = &mut s; // ✅ 借用 r1、r2 已結束
r3.push_str("!");
懸空參考:編譯器擋下來
在 C 裡很容易寫出回傳區域變數位址這種 bug。Rust 編譯期就直接拒絕:
fn dangle() -> &String {
let s = String::from("hi");
&s
} // ❌ s 在這裡 drop,回傳的參考會變成懸空
編譯錯誤訊息會給你兩條路:要嘛回傳 String(移轉所有權),要嘛調整生命週期。
切片:&str 與其他切片
切片是對連續記憶體一段範圍的借用。
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..11]; // "world"
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
&str 其實就是 string slice,所以字串字面值的型別是 &'static str——一個指向 binary 裡那段唯讀記憶體的切片。
慣例:函式參數要吃字串時宣告成 &str,比 &String 通用(&String 會自動 deref 成 &str):
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let owned = String::from("hello world");
let literal = "hello rust";
println!("{}", first_word(&owned)); // 自動 &String → &str
println!("{}", first_word(literal)); // 直接吃
}
一個完整流程的範例
fn main() {
// 1. 建立 String,s1 是所有者
let s1 = String::from("hello");
// 2. 把 s1 借給 print_len,所有權還是 s1 的
print_len(&s1);
// 3. 把 s1 move 給 s2,s1 失效
let s2 = s1;
// println!("{s1}"); // ❌
// 4. 把 s2 borrow 給 to_upper(可變),執行修改
let mut s2 = s2; // shadow 一個 mut 版本
to_upper(&mut s2);
println!("{s2}"); // HELLO
// 5. main 結束,s2 離開 scope,heap 記憶體釋放
}
fn print_len(s: &str) {
println!("len = {}", s.len());
}
fn to_upper(s: &mut String) {
*s = s.to_uppercase();
}
&mut String 要先解參考 *s 才能整個覆寫內容;或者用 s.make_ascii_uppercase() 就地改、不用重新配置。
心智模型總結
| 操作 | 語法 | 對所有權的影響 |
|---|---|---|
| 賦值 / 傳參(非 Copy 型別) | let b = a; / f(a) | move:a 失效 |
| 賦值 / 傳參(Copy 型別) | 同上 | 複製:a 還能用 |
| 不可變借用 | &a / f(&a) | a 仍是所有者,期間不能可變借用 |
| 可變借用 | &mut a / f(&mut a) | a 仍是所有者,期間不能再借任何 |
| 深拷貝 | a.clone() | 在 heap 上複製一份新資料 |
掌握這張表加上三條核心規則,所有權就走過大半了。下一篇進 struct 與 enum,那邊會看到所有權怎麼跟自訂型別互動,以及 enum 配 match 的威力。
Latest Updates
- 2026.06.11 Content updated
