本文 AI 產出,尚未審核

Rust 課程 – 結構體與方法

主題:列印結構體(Debug Trait)


簡介

在 Rust 中,**結構體(struct)**是用來描述資料的主要工具。開發過程中,我們常需要把結構體的內容印出來,無論是除錯、日誌紀錄,或是與使用者互動的介面,都離不開「把資料變成可讀的文字」這件事。

Rust 為此提供了 Debug trait,讓開發者只要為結構體「衍生」(derive) Debug,就能使用 {:?}{:#?} 這兩種格式化字串直接列印。掌握 Debug 的使用方式,不僅能大幅提升除錯效率,還能讓程式碼更具可讀性與可維護性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到實務應用,帶領讀者一步步熟悉 列印結構體 的技巧,適合從初學者到中級開發者閱讀。


核心概念

1. 為什麼需要 Debug

Rust 的格式化系統(std::fmt)採用 trait 來決定如何把值轉成字串。

  • Display:人類友好的輸出({}),需要自行實作。
  • Debug:開發者友好的輸出({:?}),大多數情況只要 #[derive(Debug)] 就能自動得到。

在除錯階段,我們往往只想快速看到結構體的欄位與值,Debug 正是為此而設計。

2. #[derive(Debug)] 的基本用法

只要在結構體(或列舉)前加上 #[derive(Debug)],編譯器會自動為每個欄位產生 Debug 實作。

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

此時,我們就可以使用 println!("{:?}", point); 直接印出 Point 的內容。

3. 美化輸出:{:?} vs {:#?}

  • {:?}:單行輸出,適合簡短資料。
  • {:#?}:多行、縮排的「pretty」輸出,適合欄位較多或巢狀結構。
let p = Point { x: 10, y: 20 };
println!("單行:{:?}", p);
println!("多行:{:#?}", p);

輸出:

單行:Point { x: 10, y: 20 }
多行:Point {
    x: 10,
    y: 20,
}

4. 自訂 Debug 實作

自動衍生的 Debug 會列印所有欄位,但有時候想隱藏敏感資訊或改寫格式,就需要手動實作 fmt

use std::fmt;

struct Secret {
    username: String,
    // 密碼不想直接印出
    password: String,
}

impl fmt::Debug for Secret {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // 只印出 username,密碼以 *** 取代
        f.debug_struct("Secret")
            .field("username", &self.username)
            .field("password", &"***")
            .finish()
    }
}

5. #[derive(Debug)] 與泛型

當結構體裡的欄位是泛型型別時,必須確保這些型別本身也實作了 Debug,才能成功衍生。

#[derive(Debug)]
struct Wrapper<T> {
    value: T,
}

// 只要 T 本身有 Debug,就能印出 Wrapper<T>
let w = Wrapper { value: vec![1, 2, 3] };
println!("{:#?}", w);

程式碼範例

以下提供 5 個實用範例,涵蓋從最簡單到進階的情境。每段程式碼皆附上說明註解,方便讀者直接 copy & paste 測試。

範例 1:最基本的 Debug 衍生

#[derive(Debug)]
struct User {
    id: u32,
    name: String,
    active: bool,
}

fn main() {
    let u = User {
        id: 1,
        name: "Alice".to_string(),
        active: true,
    };
    // 單行印出
    println!("User: {:?}", u);
    // pretty 印出
    println!("User (pretty): {:#?}", u);
}

重點:只要在結構體上加 #[derive(Debug)],即可使用 {:?}{:#?}

範例 2:巢狀結構體的列印

#[derive(Debug)]
struct Address {
    city: String,
    zip: u32,
}

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
    address: Address,
}

fn main() {
    let p = Person {
        name: "Bob".to_string(),
        age: 30,
        address: Address {
            city: "Taipei".to_string(),
            zip: 106,
        },
    };
    // pretty 印出,能清楚看到層級
    println!("{:#?}", p);
}

說明Debug 會遞迴列印所有子結構,{:#?} 讓層次更明顯。

範例 3:泛型結構體與 Debug

#[derive(Debug)]
struct Pair<T, U> {
    left: T,
    right: U,
}

fn main() {
    let pair = Pair {
        left: "hello",
        right: 3.14,
    };
    println!("{:?}", pair); // Pair { left: "hello", right: 3.14 }
}

提示:只要 TU 本身都有 DebugPair<T,U> 就能自動衍生。

範例 4:自訂 Debug 隱藏敏感資訊

use std::fmt;

#[derive(Clone)]
struct ApiKey {
    key: String,
    // 其他欄位…
}

impl fmt::Debug for ApiKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // 只顯示前四位,後面以 *** 隱藏
        let masked = if self.key.len() > 4 {
            format!("{}***", &self.key[..4])
        } else {
            "***".to_string()
        };
        f.debug_struct("ApiKey")
            .field("key", &masked)
            .finish()
    }
}

fn main() {
    let api = ApiKey {
        key: "ABCD1234EFGH".to_string(),
    };
    println!("{:?}", api); // ApiKey { key: "ABCD***" }
}

實務:在日誌或除錯時,避免直接暴露密碼、金鑰等資訊。

範例 5:在 impl 區塊中提供自訂列印方法

有時候想要在結構體上提供「友善」的列印方法,而不是每次都寫 println!("{:?}", ...)

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    use_tls: bool,
}

impl Config {
    /// 以人類可讀的方式印出設定
    fn pretty_print(&self) {
        println!(
            "Config:\n  Host: {}\n  Port: {}\n  TLS: {}",
            self.host,
            self.port,
            if self.use_tls { "Enabled" } else { "Disabled" }
        );
    }
}

fn main() {
    let cfg = Config {
        host: "example.com".to_string(),
        port: 443,
        use_tls: true,
    };
    // 使用自訂方法
    cfg.pretty_print();

    // 若仍想要 Debug 輸出
    println!("Debug view: {:#?}", cfg);
}

技巧impl 中的自訂方法可以結合 Debug,提供兩種不同層級的列印需求。


常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記加 #[derive(Debug)] 編譯時會出現 cannot format value 錯誤。 每次新增結構體時,先加上 #[derive(Debug)],或使用 IDE 快捷鍵自動補全。
泛型未實作 Debug Wrapper<T>T 沒有 Debug,衍生會失敗。 為泛型加上 trait bound:#[derive(Debug)] struct Wrapper<T: std::fmt::Debug> { … }
敏感資訊直接列印 日誌中洩漏密碼、金鑰等。 為含敏感欄位的結構體自行實作 Debug,或使用 #[debug(skip)](需要 serde/derivative 等外部 crate)。
過度使用 println! 在大型專案中,過多 println! 會影響效能與可讀性。 使用 log crate(info!, debug!, error!)配合 env_logger,在需要時才啟用 Debug 輸出。
忘記使用 {:#?} 複雜結構印成單行,難以閱讀。 在除錯階段切換到 pretty 模式,或在 IDE 中使用「Inspect」功能。

最佳實踐總結

  1. 預設衍生:在所有公共結構體上預設 #[derive(Debug, Clone, PartialEq)],減少遺漏。
  2. 自訂隱私:對敏感欄位自行實作 Debug,避免意外洩漏。
  3. 統一日誌:使用 log 生態系統,將 Debug 輸出納入可設定的日誌層級。
  4. 測試支援:在單元測試中使用 assert_eq!(format!("{:?}", obj), "...") 來驗證結構體的內容。

實際應用場景

  1. 開發階段的快速除錯

    • 在函式內部 println!("{:?}", data);,即時觀察資料流向。
  2. 服務端 API 日誌

    • 將收到的 JSON 反序列化為結構體後,用 debug! 記錄 {:?},方便追蹤錯誤請求。
  3. CLI 工具的 --debug 旗標

    • 當使用者開啟 --debug 時,印出內部結構體的 Debug 表示,協助使用者回報問題。
  4. 測試斷言

    • assert_eq!(format!("{:?}", result), "Expected { … }");,直接比較列印結果,避免手寫繁雜的欄位比對。
  5. 嵌入式或系統程式

    • 在資源受限環境下,使用 core::fmt::WriteDebug 輸出寫入 UART,快速定位硬體錯誤。

總結

  • Debug trait 是 Rust 中最常用的除錯工具,只要 #[derive(Debug)],就能立即得到結構體的文字化表示。
  • 使用 {:#?} 可以得到 多行、易讀 的輸出;若需保護敏感資訊,則自行實作 fmt,或使用外部 crate 進行欄位跳過。
  • 在泛型、巢狀結構與日誌系統中,正確地把 Debuglog 結合,能讓除錯過程更安全、更高效。

掌握了 Debug 的使用與最佳實踐後,你將能在開發、測試與部署的每個階段,快速定位問題、提升程式碼可讀性,並在實務專案中保持資訊安全。祝你在 Rust 的旅程中,寫出更乾淨、更可靠的程式!