本文 AI 產出,尚未審核

Rust 課程 – 結構體與方法

主題:結構體的所有權與借用


簡介

在 Rust 中,所有權(ownership)借用(borrowing) 是語言安全性的核心機制。它們不僅保證了記憶體安全,還讓編譯器在編譯階段就能偵測出許多常見的錯誤。當我們把資料封裝進 結構體(struct) 時,所有權的規則仍然適用,而結構體本身也會成為借用的單位。

如果不了解結構體與所有權、借用之間的互動,就很容易在撰寫 API、實作方法或傳遞資料時碰到「值已被移動」或「不可變參照與可變參照同時存在」的編譯錯誤。本文將從概念說明、實作範例、常見陷阱到實務應用,完整闡述 結構體的所有權與借用,協助初學者與中階開發者在寫 Rust 程式時更得心應手。


核心概念

1️⃣ 結構體的所有權規則

規則 說明
每個值都有唯一的所有者 結構體本身是一個值,當它被賦值或傳遞時,所有權會被 移動(move)複製(copy)(若類型實作 Copy)。
所有權只能有一個活躍的持有者 移動後,原本的變數將不再可用。
當所有者離開作用域時,資源會被釋放 結構體的 Drop 實作會在作用域結束時自動呼叫。

範例 1:結構體所有權的移動

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    // p1 的所有權被移動到 p2
    let p2 = p1;          // ← p1 失效,若再使用會編譯錯誤
    // println!("{:?}", p1); // 編譯錯誤:value used after move
    println!("p2: ({}, {})", p2.x, p2.y);
}

重點Point 沒有實作 Copy,所以賦值會觸發 所有權移動


2️⃣ 借用結構體:不可變與可變參照

Rust 允許我們 借用 結構體的所有權,而不必真的把它移走。借用分為兩種:

  • &T:不可變參照,同時可以有多個
  • &mut T:可變參照,同一時間只能有一個,且不能與任何不可變參照同時存在。

範例 2:不可變借用

fn print_point(p: &Point) {
    // 只能讀取,不能修改
    println!("x = {}, y = {}", p.x, p.y);
}

fn main() {
    let p = Point { x: 5, y: 8 };
    print_point(&p);   // 借用 p 的不可變參照
    // 此時仍可再次借用或直接使用 p
    println!("still can use p: ({}, {})", p.x, p.y);
}

範例 3:可變借用

fn move_point(p: &mut Point, dx: i32, dy: i32) {
    p.x += dx;
    p.y += dy;
}

fn main() {
    let mut p = Point { x: 0, y: 0 };
    move_point(&mut p, 3, 4);   // 可變借用
    println!("moved: ({}, {})", p.x, p.y);
}

注意move_point 必須接受 &mut Point,因為它要改變結構體的內部欄位。


3️⃣ 方法接收者 (self) 的所有權與借用

impl 區塊裡,我們可以為結構體定義三種常見的 方法接收者

接收者 語意 何時使用
self 取得所有權,方法結束後值會被移走或被回傳。 需要消耗結構體、或回傳自身(例如 into_inner)。
&self 不可變借用,只能讀取。 只需要讀取資料,且允許同時多個呼叫。
&mut self 可變借用,允許修改。 需要改變結構體內部狀態。

範例 4:三種接收者的比較

impl Point {
    // 取得所有權,回傳自身的 x 座標
    fn into_x(self) -> i32 {
        self.x               // self 被消費,方法結束後 p 不再可用
    }

    // 不可變借用,只讀取
    fn distance_from_origin(&self) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }

    // 可變借用,改變座標
    fn translate(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let p = Point { x: 3, y: 4 };
    let d = p.distance_from_origin(); // &self
    println!("distance = {}", d);

    let mut p2 = Point { x: 1, y: 2 };
    p2.translate(5, 5);                // &mut self
    println!("translated = ({}, {})", p2.x, p2.y);

    let x = p2.into_x();                // self(所有權被消費)
    // println!("{:?}", p2); // 編譯錯誤:value used after move
    println!("x = {}", x);
}

4️⃣ 結構體裡的引用與生命週期(Lifetime)

當結構體的欄位本身是引用時,生命週期 必須明確標註,否則編譯器無法保證引用在結構體使用期間仍然有效。

struct RefPoint<'a> {
    x: &'a i32,
    y: &'a i32,
}

fn make_ref_point<'a>(a: &'a i32, b: &'a i32) -> RefPoint<'a> {
    RefPoint { x: a, y: b }
}

fn main() {
    let a = 10;
    let b = 20;
    let rp = make_ref_point(&a, &b);
    println!("ref point: ({}, {})", rp.x, rp.y);
}

技巧:若結構體只在短暫的作用域內使用,盡量避免 把引用放進結構體;改用擁有所有權的類型(如 StringVec<T>)會讓程式更容易管理。


5️⃣ 常見的所有權/借用組合

情境 正確寫法 常見錯誤
將結構體傳入只讀函式 fn foo(p: &MyStruct) { … } 並呼叫 foo(&s); 直接傳 s(移動)導致後續無法使用
在同一作用域同時持有 &mut& 先結束可變借用(作用域結束)再建立不可變借用 同時持有 &mut s&s 編譯錯誤
返回引用 必須使用生命週期參數,或返回擁有所有權的值 返回局部變數的引用導致編譯錯誤

常見陷阱與最佳實踐

🚩 陷阱 1:不小心移動結構體

let a = Point { x: 1, y: 2 };
let b = a;          // a 已被移動
println!("{:?}", a); // ❌ 編譯錯誤

解法:若只想「共享」而不消耗,使用借用 &a&mut a;若需要多個擁有者,可考慮 Rc<T>(單執行緒)或 Arc<T>(多執行緒)。

🚩 陷阱 2:同時持有可變與不可變參照

let mut p = Point { x: 0, y: 0 };
let r1 = &p;        // 不可變借用
let r2 = &mut p;    // ❌ 同時持有可變借用

最佳實踐縮小借用的作用域,或使用 RefCell<T> 於單執行緒環境下提供「執行時」的可變借用檢查。

🚩 陷阱 3:在結構體中存放引用卻忘記生命週期

struct Bad<'a> { data: &'a str }

fn create() -> Bad<'static> {   // 錯誤的生命週期標註
    let s = String::from("hello");
    Bad { data: &s }            // s 會在此函式結束後被釋放
}

解法避免在需要長期保存的結構體裡使用引用;改用 StringVec<T> 等擁有所有權的型別,或明確傳入外部生命週期較長的資料。

✅ 最佳實踐總結

建議 為什麼
盡量讓結構體擁有自己的資料StringVec<T> 減少生命週期管理的複雜度
使用 &self 作為只讀方法的接收者 允許同時多個呼叫,提高併發性
使用 &mut self 只在必要時 防止不必要的可變借用衝突
對於需要共享所有權的情境,使用 Rc<T> / Arc<T> 避免所有權移動導致的「使用後已被移動」錯誤
在需要在執行時改變借用規則時,考慮 RefCell<T> / Mutex<T> 提供內部可變性,同時保留編譯期安全檢查

實際應用場景

1️⃣ 資料模型與序列化

在 Web 服務或 CLI 工具中,我們常把資料模型(struct)序列化成 JSON、MessagePack 等格式。序列化函式通常接受 &self,因為它只需要讀取資料:

use serde::Serialize;
use serde_json;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

impl User {
    fn to_json(&self) -> String {
        serde_json::to_string(self).unwrap()
    }
}

2️⃣ 互動式圖形介面(GUI)中的狀態管理

在即時渲染或遊戲開發中,畫面狀態 常以結構體保存。渲染函式會取得 &self,而更新函式則需要 &mut self

struct GameState {
    player_x: f32,
    player_y: f32,
    score: u32,
}

impl GameState {
    fn render(&self) {
        // 只讀取狀態,繪製畫面
    }

    fn update(&mut self, dt: f32) {
        // 根據時間 dt 改變位置
        self.player_x += dt * 5.0;
    }
}

3️⃣ 多執行緒任務佇列

使用 Arc<Mutex<T>> 包裝共享的結構體,讓多個執行緒可以安全地 取得可變借用

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

#[derive(Debug)]
struct Counter {
    value: usize,
}

fn main() {
    let counter = Arc::new(Mutex::new(Counter { value: 0 }));
    let mut handles = vec![];

    for _ in 0..5 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut guard = c.lock().unwrap(); // 取得可變借用
            guard.value += 1;
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("final count = {}", counter.lock().unwrap().value);
}

總結

  • 所有權與借用是 Rust 保證記憶體安全的根本,結構體不例外。
  • 了解 self&self&mut self 三種方法接收者的差異,能幫助我們設計出 清晰、可預測 的 API。
  • 當結構體內部包含 引用 時,必須正確標註 生命週期,或盡量使用擁有所有權的型別以降低複雜度。
  • 常見的錯誤包括 所有權意外移動同時持有可變與不可變借用、以及 錯誤的生命週期標註,只要遵守 「借用要麼不可變,要麼唯一可變」 的原則,就能避免大多數編譯錯誤。
  • 在實務開發中,資料模型、即時渲染、跨執行緒共享 等情境都會頻繁碰到結構體的所有權與借用問題,熟練本章內容將直接提升程式的可讀性、效能與安全性。

**掌握結構體的所有權與借用,是成為 Rust 高手的必經之路。**只要在設計時先思考「誰擁有這筆資料?」、「何時需要可變或不可變的存取?」,就能寫出既安全又高效的程式碼。祝你在 Rust 的旅程中玩得開心,寫出更多可靠的程式!