Rust 課程:所有權與借用 ── 移動(Move)與複製(Copy)
簡介
在 Rust 中,**所有權(Ownership)**是語言安全性的核心機制。它不僅決定了值在記憶體中的生命週期,也直接影響到程式的效能與併發安全。
其中最常被提及的兩個操作是 移動(move) 與 複製(copy):前者把資源的所有權從一個變數轉移到另一個變數,後者則在不改變原有所有權的前提下,產生一個「淺層」的副本。
了解何時會發生移動、何時會自動複製,對於避免編譯錯誤、寫出高效且安全的程式碼至關重要。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 Rust 中的 move 與 copy。
核心概念
1. 所有權的基本規則
- 每個值都有唯一的所有者(變數)。
- 所有者離開作用域時,值會被自動釋放(Drop)。
- 同一時間只能有一個所有者,除非值實作了
Copytrait。
這三條規則讓 Rust 能在編譯期就檢查出大部分的記憶體安全問題。
2. 移動(Move)
當一個變數的值被賦予給另一個變數,且該類型 沒有實作 Copy,編譯器會執行 move,把所有權從舊變數「搬走」:
fn main() {
let s1 = String::from("hello"); // s1 為所有者
let s2 = s1; // 移動所有權到 s2
// println!("{}", s1); // ❌ 編譯錯誤:s1 已失去所有權
println!("{}", s2); // 正常
}
String包含堆積記憶體,搬走所有權後,舊變數會變成「未初始化」的狀態,任何使用都會被編譯器阻止。
為什麼需要移動?
- 避免雙重釋放:如果兩個變數都持有同一塊記憶體的所有權,程式結束時會嘗試釋放兩次,造成未定義行為。移動保證了唯一所有權。
- 零成本抽象:移動僅是指標的複製,沒有額外的記憶體拷貝成本。
3. 複製(Copy)
若型別實作了 Copy trait,賦值時會 自動產生位元層面的副本,而不會移動所有權。Rust 為以下類型自動實作 Copy:
- 所有 標量類型(整數、浮點數、布林、字元、指標等)
- 陣列(若元素皆為
Copy) - 元組(若所有成員皆為
Copy)
fn main() {
let a: i32 = 10; // a 為所有者
let b = a; // 複製,a 仍然有效
println!("a = {}, b = {}", a, b); // 兩者皆可使用
}
若想讓自訂結構實作 Copy,必須確保所有欄位也都是 Copy,並在宣告時加入 #[derive(Copy, Clone)]:
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // 直接複製
println!("{:?} -> {:?}", p1, p2);
}
注意:
Copy只能用於「位元拷貝安全」的類型,像是String、Vec<T>等擁有堆積資源的型別不能自動實作Copy,必須手動實作Clone(深拷貝)或使用move。
4. Clone 與 Copy 的差別
| 特性 | Copy |
Clone |
|---|---|---|
| 產生方式 | 位元拷貝(編譯期完成) | 自訂邏輯(執行期) |
| 成本 | 零成本(僅指標/值的複製) | 可能涉及堆積分配、深拷貝 |
| 必須實作 | 只能對「簡單」型別自動實作 | 任意型別皆可手動實作 |
| 使用情境 | 小型、頻繁傳遞的資料(如座標、索引) | 大型、需要深拷貝的結構(如字串、向量) |
5. 移動與借用的結合
在實務開發中,常會同時使用 move 與 借用(borrow):
fn print_len(s: &String) {
println!("長度是 {}", s.len());
}
fn main() {
let s = String::from("Rust");
print_len(&s); // 借用,不移動所有權
// s 仍然可以使用
println!("原始字串:{}", s);
}
若想把所有權「搬走」同時又保留原始值,可先 clone 再 move:
fn consume(s: String) {
println!("消耗字串:{}", s);
}
fn main() {
let original = String::from("Hello");
let cloned = original.clone(); // 深拷貝
consume(cloned); // 移動 cloned 的所有權
// original 仍然可用
println!("仍在此:{}", original);
}
程式碼範例
以下提供 5 個實用範例,展示 move、copy、clone 的不同情境。
範例 1:基本移動與編譯錯誤
fn main() {
let v1 = vec![1, 2, 3]; // Vec<T> 不實作 Copy
let v2 = v1; // 移動所有權
// println!("{:?}", v1); // ❌ 編譯錯誤:v1 已被移走
println!("v2 = {:?}", v2);
}
重點:
Vec<T>內部有堆積記憶體,必須透過移動保證唯一所有權。
範例 2:Copy 的標量類型
fn main() {
let a: u8 = 255;
let b = a; // 位元拷貝
println!("a = {a}, b = {b}");
}
說明:
u8為標量類型,自動實作Copy,賦值不會失去所有權。
範例 3:自訂結構的 Copy 與 Clone
#[derive(Copy, Clone, Debug)]
struct Color {
r: u8,
g: u8,
b: u8,
}
fn main() {
let red = Color { r: 255, g: 0, b: 0 };
let another = red; // 直接 Copy
println!("{:?} -> {:?}", red, another);
}
提示:若結構中包含
String或Vec<T>,只能derive(Clone),不能Copy。
範例 4:使用 clone 取得深拷貝
fn main() {
let s1 = String::from("Rust");
let s2 = s1.clone(); // 深拷貝,分配新記憶體
println!("s1 = {s1}, s2 = {s2}");
}
效能注意:
clone會在執行期分配記憶體,若頻繁使用可能成為瓶頸。
範例 5:在函式中搬移與借用的混合
fn take_ownership(v: Vec<i32>) {
println!("取得所有權:{:?}", v);
}
fn borrow(v: &Vec<i32>) {
println!("借用,不取得所有權:{:?}", v);
}
fn main() {
let data = vec![10, 20, 30];
borrow(&data); // 借用,data 仍然有效
let data_clone = data.clone(); // 深拷貝
take_ownership(data_clone); // 搬移 clone 的所有權
// data 仍然可以使用
println!("原始 data:{:?}", data);
}
實務觀點:在需要保留原始資料同時又要傳遞所有權的情況下,clone + move 是常見模式。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 誤以為賦值會自動複製 | 對非 Copy 類型使用 let b = a; 會移動所有權,導致後續使用 a 錯誤。 |
使用 clone() 或改為借用 &a。 |
過度使用 clone |
為了避免移動而大量 clone,會產生不必要的記憶體分配與拷貝。 |
先思考是否可以改用 借用 或 引用計數(Rc<T>、Arc<T>)。 |
自訂結構忘記 Copy/Clone |
結構在需要頻繁傳遞時若未實作 Copy,會不小心觸發移動。 |
為純值結構加上 #[derive(Copy, Clone)],或使用 #[derive(Clone)] 並手動 clone()。 |
| 在迴圈中搬移容器 | for v in vec 會把 vec 的每個元素搬走,迴圈結束後 vec 為空。 |
使用 for v in &vec(借用)或 vec.iter().cloned()。 |
| 混用可變與不可變借用 | 同時持有可變借用與不可變借用會編譯錯誤。 | 確保在同一作用域內只存在一種借用,或使用 RefCell<T> 進行內部可變性。 |
最佳實踐:
- 先考慮借用:如果只需要讀取或暫時使用資料,盡量使用
&或&mut,避免不必要的搬移或複製。 - 只在必要時
clone:評估是否真的需要深拷貝,或可以改用共享指標(Rc/Arc)或Cow(Copy‑On‑Write)。 - 利用
Copy的零成本特性:對於小型、頻繁傳遞的資料(座標、索引、旗標),使用Copy可提升效能且語意清晰。 - 遵守所有權規則:讓編譯器幫你檢查記憶體安全,避免手動管理記憶體的錯誤。
實際應用場景
| 場景 | 為何需要 Move | 為何需要 Copy |
|---|---|---|
資料結構的建構(如 Vec<T>、HashMap<K,V>) |
把外部的 String、Vec 等搬入容器,確保容器成為唯一所有者。 |
在容器內存放小型鍵值(如 u32、bool)時,使用 Copy 可避免每次插入時的額外拷貝。 |
多執行緒傳遞(std::thread::spawn) |
spawn 需要取得閉包的所有權,必須 move 捕獲的變數。 |
若傳遞的資料僅是簡單的計數器或旗標,使用 Copy 可直接傳遞而不必額外 clone。 |
| 圖形渲染引擎 | 大型資源(貼圖、模型)使用 Arc<T> 或 Rc<T>,但在需要唯一寫入權時仍會 move。 |
顏色、向量、矩陣等小型結構多使用 Copy,在渲染迴圈中頻繁傳遞。 |
| 解析器/編譯器 | 把字串切片(&str)搬入抽象語法樹(AST)節點時,通常會 move 所有權。 |
Token 的類型(TokenKind)通常是 Copy 的 enum,快速傳遞。 |
總結
- 移動(move):將所有權從一個變數轉移到另一個變數,確保同一時間只有唯一所有者,避免雙重釋放與記憶體安全問題。
- 複製(copy):對於實作
Copy的類型,賦值時會產生位元層面的副本,且不影響原所有權,成本接近零。 - Clone:提供深拷貝能力,適用於需要真正複製堆積資源的情況,但會有執行期成本。
- 最佳實踐:優先使用借用 (
&、&mut);僅在必要時才move或clone;對小型、頻繁傳遞的資料使用Copy,提升效能與可讀性。
掌握了 move 與 copy 的差異與使用時機,你就能寫出既安全又高效的 Rust 程式,並在實務開發中靈活運用所有權模型,避免常見的記憶體錯誤。祝你在 Rust 的旅程中玩得開心、寫得安心!