本文 AI 產出,尚未審核

Rust 課程 – 結構體與方法

主題:結構體(Structs)定義


簡介

在 Rust 中,結構體(struct)是用來把多個相關資料組合成一個自訂型別的基礎工具。它不僅讓程式碼更具可讀性,也為後續的方法(method)trait實作提供了載體。對於想要寫出安全、效能佳且易於維護的程式,掌握結構體的定義與使用是必不可少的步驟。

本單元將從最簡單的「tuple struct」談起,逐步深入到具名欄位的「classic struct」與「unit‑like struct」。每個概念都會配合實作範例,說明 為什麼 以及 怎麼 在真實專案中使用它們。


核心概念

1. 為什麼要使用結構體?

  • 資料封裝:把相關的欄位聚合在一起,避免散落在全域變數或函式參數中。
  • 型別安全:Rust 的編譯器會檢查每個欄位的型別,減少因錯誤傳遞資料而產生的 bug。
  • 可擴充性:日後若需要新增欄位,只要在 struct 定義中加入即可,呼叫端只需要更新建構子或使用具名參數。

2. 結構體的三種基本形態

形態 語法範例 使用情境
Unit‑like struct struct Empty; 僅作為「標記」或「型別」使用,例如實作 trait 的旗標。
Tuple struct struct Color(u8, u8, u8); 欄位較少且不需要欄位名稱時,類似 C 的 struct
Classic struct struct Person { name: String, age: u8 } 最常見,欄位名稱清晰、可自行命名。

以下分別說明每種形態的寫法與注意點。


2.1 Unit‑like struct

// 定義一個空結構體,常用來作為 trait 的實作標記
struct Marker;

// 為 Marker 實作一個空的 trait
trait Printable {}
impl Printable for Marker {}

fn main() {
    // 雖然 Marker 沒有任何欄位,但仍然可以建立實例
    let _m = Marker;
    println!("Marker 已建立,並可用於 trait bound");
}

重點:即使沒有欄位,Rust 仍會為它產生唯一的型別,適合在 泛型約束 中使用。


2.2 Tuple struct

// 定義一個代表 RGB 顏色的 tuple struct
struct Rgb(u8, u8, u8);

impl Rgb {
    // 取得每個顏色分量的 getter
    fn red(&self) -> u8 { self.0 }
    fn green(&self) -> u8 { self.1 }
    fn blue(&self) -> u8 { self.2 }
}

fn main() {
    let sky = Rgb(135, 206, 235);
    println!("天空的藍色分量是 {}", sky.blue());
}
  • 欄位存取:使用 .0, .1, .2 直接索引。
  • 適用情境:資料結構簡單且不需要欄位名稱的情況,如 座標點錯誤碼 等。

2.3 Classic struct

// 定義一個描述使用者的結構體
#[derive(Debug)]
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

// 建構子(使用具名參數)
// 注意:欄位的順序不必與 struct 定義相同
fn new_user(name: &str, mail: &str) -> User {
    User {
        username: name.to_string(),
        email: mail.to_string(),
        sign_in_count: 1,
        active: true,
    }
}

fn main() {
    let mut alice = new_user("alice", "alice@example.com");
    println!("{:?}", alice);

    // 使用可變借用修改欄位
    alice.email = "alice@newdomain.org".to_string();
    println!("更新後的 email: {}", alice.email);
}
  • 具名欄位 讓程式碼更具可讀性。
  • #[derive(Debug)] 為結構體自動產生 fmt::Debug 實作,方便 println!("{:?}", ...) 調試。
  • 可變性:若要修改欄位,必須將實例宣告為 mut,且欄位本身不可是 &(借用)或 &mut(可變借用)除非使用 所有權 轉移。

3. 結構體的所有權與借用

#[derive(Debug)]
struct Book {
    title: String,
    author: String,
}

// 取得 Book 的所有權
fn take_book(b: Book) {
    println!("我拿到了書:{:?}", b);
}

// 只借用 Book,允許呼叫端繼續使用
fn borrow_book(b: &Book) {
    println!("我只借用了書:{} by {}", b.title, b.author);
}

fn main() {
    let my_book = Book {
        title: "Rust 程式設計".to_string(),
        author: "王小明".to_string(),
    };

    borrow_book(&my_book); // 只借用,不會移動所有權
    take_book(my_book);    // 移動所有權,my_book 後面不可再使用
    // println!("{:?}", my_book); // 這行會編譯錯誤
}
  • 所有權轉移(move)會使原變數失效,必須小心使用。
  • 借用(reference)則允許多個讀取或單一可變讀取,符合 Rust 的安全模型

4. 結構體的可見性(Visibility)

mod user {
    // 只公開 struct 名稱,欄位保持私有
    pub struct Profile {
        pub username: String,
        email: String, // 私有欄位
    }

    impl Profile {
        // 公開建構子,內部自行設定私有欄位
        pub fn new(name: &str, mail: &str) -> Self {
            Profile {
                username: name.to_string(),
                email: mail.to_string(),
            }
        }

        // 公開 getter 取得私有欄位
        pub fn email(&self) -> &str {
            &self.email
        }
    }
}

fn main() {
    let p = user::Profile::new("bob", "bob@example.com");
    println!("使用者名稱:{}", p.username);
    // println!("Email:{}", p.email); // 直接存取會編譯錯誤
    println!("Email:{}", p.email());
}
  • 使用 pub 關鍵字控制 模組層級的可見性
  • 常見做法是 欄位私有、提供公開的 getter / setter,以維持不變性(immutability)或加入驗證邏輯。

常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記 mut 想要修改結構體欄位卻忘記在變數前加 mut,編譯錯誤。 宣告 let mut foo = ...,或使用 可變借用 &mut foo
所有權意外移動 把結構體傳給函式時不小心移動所有權,導致後續使用失效。 使用 參考&)或 clonefoo.clone())來保留所有權。
欄位名稱衝突 在同一模組內定義多個相同欄位名稱的 struct,易混淆。 命名空間(module)或 前綴 方式區分,例如 user::Profile vs admin::Profile
過度暴露欄位 把所有欄位都設為 pub,失去封裝與驗證的機會。 欄位私有化,只公開必要的 getter / setter。
忘記 #[derive(Debug)] 想要印出 struct 卻忘記導入 Debug,編譯錯誤。 為常用的 struct 加上 #[derive(Debug, Clone, PartialEq)] 等常見 trait。

最佳實踐

  1. 盡量使用具名欄位的 classic struct,提升可讀性。
  2. 保持欄位私有,透過方法提供受控的存取。
  3. 為重要的 struct 加上 #[derive(Debug, Clone, PartialEq)],減少手動實作的負擔。
  4. 利用 newbuilder pattern 建構複雜的結構體,避免長參數列。
  5. 在模組邊界明確劃分公開介面,讓外部只能看到必要的型別與方法。

實際應用場景

場景 可能的 struct 設計 為什麼適合使用 struct
Web 伺服器的請求資料 struct HttpRequest { method: Method, path: String, headers: HashMap<String, String>, body: Vec<u8> } 把所有請求資訊聚合,方便傳遞給中介層(middleware)或路由處理器。
遊戲開發的座標與向量 struct Vec3(f32, f32, f32);(tuple struct)或 struct Position { x: f32, y: f32, z: f32 } 需要大量計算且欄位固定,tuple struct 可減少記憶體占用與程式碼冗長。
資料庫 ORM 的模型 #[derive(Debug, Clone)] struct User { id: i32, name: String, email: String, created_at: NaiveDateTime } 每一筆資料對應一個 struct,結合 serdesqlx 等 crate 可自動序列化/反序列化。
多執行緒共享設定 pub struct Config { pub max_threads: usize, pub log_level: LogLevel }(欄位公開,配合 Arc<Config> 允許多個執行緒讀取相同設定,使用 Arc 共享所有權。
標記型別(Phantom Types) struct Meter<T>(f64, std::marker::PhantomData<T>); 透過 unit‑like struct 結合 PhantomData,在編譯期提供單位檢查,避免單位混淆。

總結

  • 結構體是 Rust 中最基礎且最強大的資料聚合工具,從簡單的 unit‑liketuple 到完整的 classic,每種形態都有其適用情境。
  • 正確掌握 所有權、借用與可見性,能讓你寫出安全且易於維護的程式。
  • 避免常見的所有權移動與可變性錯誤,並遵循 封裝導出必要介面使用 derive 等最佳實踐,將大幅提升開發效率。
  • 在實務上,從 Web 請求遊戲座標資料庫模型多執行緒設定,結構體都是不可或缺的核心概念。

掌握了結構體的定義與使用,你就能在 Rust 程式碼中建立清晰、可擴充且符合安全保證的資料模型,為後續的 方法(method)trait泛型 打下堅實基礎。祝你在 Rust 的旅程中寫出更好、更安全的程式!