本文 AI 產出,尚未審核

Rust 課程:所有權與借用 ── 移動(Move)與複製(Copy)


簡介

在 Rust 中,**所有權(Ownership)**是語言安全性的核心機制。它不僅決定了值在記憶體中的生命週期,也直接影響到程式的效能與併發安全。
其中最常被提及的兩個操作是 移動(move)複製(copy):前者把資源的所有權從一個變數轉移到另一個變數,後者則在不改變原有所有權的前提下,產生一個「淺層」的副本。

了解何時會發生移動、何時會自動複製,對於避免編譯錯誤、寫出高效且安全的程式碼至關重要。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 Rust 中的 move 與 copy。


核心概念

1. 所有權的基本規則

  1. 每個值都有唯一的所有者(變數)。
  2. 所有者離開作用域時,值會被自動釋放(Drop)。
  3. 同一時間只能有一個所有者,除非值實作了 Copy trait。

這三條規則讓 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 只能用於「位元拷貝安全」的類型,像是 StringVec<T> 等擁有堆積資源的型別不能自動實作 Copy,必須手動實作 Clone(深拷貝)或使用 move


4. CloneCopy 的差別

特性 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);
}

若想把所有權「搬走」同時又保留原始值,可先 clonemove

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);
}

提示:若結構中包含 StringVec<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> 進行內部可變性。

最佳實踐

  1. 先考慮借用:如果只需要讀取或暫時使用資料,盡量使用 &&mut,避免不必要的搬移或複製。
  2. 只在必要時 clone:評估是否真的需要深拷貝,或可以改用共享指標(Rc/Arc)或 Cow(Copy‑On‑Write)。
  3. 利用 Copy 的零成本特性:對於小型、頻繁傳遞的資料(座標、索引、旗標),使用 Copy 可提升效能且語意清晰。
  4. 遵守所有權規則:讓編譯器幫你檢查記憶體安全,避免手動管理記憶體的錯誤。

實際應用場景

場景 為何需要 Move 為何需要 Copy
資料結構的建構(如 Vec<T>HashMap<K,V> 把外部的 StringVec 等搬入容器,確保容器成為唯一所有者。 在容器內存放小型鍵值(如 u32bool)時,使用 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);僅在必要時才 moveclone;對小型、頻繁傳遞的資料使用 Copy,提升效能與可讀性。

掌握了 movecopy 的差異與使用時機,你就能寫出既安全又高效的 Rust 程式,並在實務開發中靈活運用所有權模型,避免常見的記憶體錯誤。祝你在 Rust 的旅程中玩得開心、寫得安心!