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 }
}
提示:只要
T、U本身都有Debug,Pair<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」功能。 |
最佳實踐總結
- 預設衍生:在所有公共結構體上預設
#[derive(Debug, Clone, PartialEq)],減少遺漏。 - 自訂隱私:對敏感欄位自行實作
Debug,避免意外洩漏。 - 統一日誌:使用
log生態系統,將Debug輸出納入可設定的日誌層級。 - 測試支援:在單元測試中使用
assert_eq!(format!("{:?}", obj), "...")來驗證結構體的內容。
實際應用場景
開發階段的快速除錯
- 在函式內部
println!("{:?}", data);,即時觀察資料流向。
- 在函式內部
服務端 API 日誌
- 將收到的 JSON 反序列化為結構體後,用
debug!記錄{:?},方便追蹤錯誤請求。
- 將收到的 JSON 反序列化為結構體後,用
CLI 工具的
--debug旗標- 當使用者開啟
--debug時,印出內部結構體的Debug表示,協助使用者回報問題。
- 當使用者開啟
測試斷言
assert_eq!(format!("{:?}", result), "Expected { … }");,直接比較列印結果,避免手寫繁雜的欄位比對。
嵌入式或系統程式
- 在資源受限環境下,使用
core::fmt::Write把Debug輸出寫入 UART,快速定位硬體錯誤。
- 在資源受限環境下,使用
總結
Debugtrait 是 Rust 中最常用的除錯工具,只要#[derive(Debug)],就能立即得到結構體的文字化表示。- 使用
{:#?}可以得到 多行、易讀 的輸出;若需保護敏感資訊,則自行實作fmt,或使用外部 crate 進行欄位跳過。 - 在泛型、巢狀結構與日誌系統中,正確地把
Debug與log結合,能讓除錯過程更安全、更高效。
掌握了 Debug 的使用與最佳實踐後,你將能在開發、測試與部署的每個階段,快速定位問題、提升程式碼可讀性,並在實務專案中保持資訊安全。祝你在 Rust 的旅程中,寫出更乾淨、更可靠的程式!