Rust程式語言內建的Debug
特性非常方便,可以直接將任意型別的值以字串的方式顯示出來,而且還可以支援一定程度的格式化方式。在一般的情況下,要替我們自己的一個型別實作Debug
特性,只需在其derive
屬性加上Debug
參數就好了,沒有什麼難度。但是在比較特別的情況下,我們就無法用derive
屬性來實作Debug
特性。
有關於Rust程式語言格式化文字的方式,可以參考《Rust學習之路》系列文章的格式化文字章節。
以下是一個可以直接用derive
屬性來實作Debug
特性的例子:
#[derive(Debug)]
struct MyStruct {
f1: u8,
f2: i16,
f3: f64,
f4: String,
}
如果我們再撰寫以下程式:
let s = MyStruct {
f1: 1,
f2: 2,
f3: 3.0,
f4: "4".to_string()
};
println!("{s:05?}");
println!("{s:#05?}");
執行結果如下:
MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4" } MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4", }
但如果我們把型別為Box<dyn Display>
的欄位加進這個MyStruct
結構體中的話,使用#[derive(Debug)]
屬性時就會無法編譯。
use std::fmt::Display;
#[derive(Debug)]
struct MyStruct {
f1: u8,
f2: i16,
f3: f64,
f4: String,
f5: Box<dyn Display>, // compilation error
}
這是因為Box<dyn Display>
型別並沒有去實作Debug
特性,所以#[derive(Debug)]
屬性不知道要怎麼去將Box<dyn Display>
型別轉成字串。此時我們就要手動實作Debug
特性了。
實作Debug
特性的程式結構如下:
use std::fmt::{self, Debug, Formatter};
impl Debug for MyStruct {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
...
}
}
稍微介紹一下幾個Formatter
結構實體比較重要的方法:
alternate
:判斷在格式化字串的時候是否有使用井字號#
,例如{:#?}
。通常這個井字號#
用在Debug
特性時,表示要讓輸出的文字變得更整齊。width
:取得格式化字串的時候設定的文字寬度,例如{:5?}
。alternate
:取得格式化字串的時候設定的文字對齊方式,例如{:<5?}
。precision
:取得格式化字串的時候設定的數值精準度,例如{:<5.2?}
。fill
:取得格式化字串的時候設定的填充字元,例如{:@>5?}
。pad
:可以傳入一個字串切片,並且可以根據width
、precision
、fill
的資訊對其做「填充」(padding)的動作。sign_plus
:取得格式化字串的時候是否有使用正號+
,例如{:+?}
。sign_minus
:取得格式化字串的時候是否有使用負號+
,例如{:-?}
。sign_aware_zero_pad
:取得格式化字串的時候是否要用0
來填充,例如{:05?}
。
如何?有沒有覺得很複雜?如果我們試著自行簡單實作Debug
特性,可以寫出如下的程式:
use std::fmt::{self, Debug, Formatter};
impl Debug for MyStruct {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("MyStruct {{ f1: {:?}, f2: {:?}, f3: {:?}, f4: {:?}, f5: {} }}", self.f1, self.f2, self.f3, self.f4, self.f5.to_string()))
}
}
當然,如果只是這樣實作的話,就沒有使用#[derive(Debug)]
屬性來實作Debug
特性那樣,擁有強大的格式化功能。例如以下程式,
let s = MyStruct {
f1: 1,
f2: 2,
f3: 3.0,
f4: "4".to_string(),
f5: Box::new("5")
};
println!("{:05?}", s);
println!("{:#05?}", s);
執行結果如下:
MyStruct { f1: 1, f2: 2, f3: 3, f4: "4", f5: 5 } MyStruct { f1: 1, f2: 2, f3: 3, f4: "4", f5: 5 }
如果要讓型別的欄位也可以擁有原先的格式化功能的話,可以考慮改用Formatter
結構實體提供的debug_*
方法來實作Debug
特性。我們可以將Debug
特性的程式實作改寫如下:
use std::fmt::{self, Debug, Formatter};
impl Debug for MyStruct {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("MyStruct");
builder.field("f1", &self.f1);
builder.field("f2", &self.f2);
builder.field("f3", &self.f3);
builder.field("f4", &self.f4);
builder.field("f5", &self.f5.to_string());
builder.finish()
}
}
然後再執行以下程式看看:
let s = MyStruct {
f1: 1,
f2: 2,
f3: 3.0,
f4: "4".to_string(),
f5: Box::new("5")
};
println!("{:05?}", s);
println!("{:#05?}", s);
執行結果如下:
MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4", f5: "5" } MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4", f5: "5", }
這樣雖然能讓實作出來的Debug
特性有格式化的功能,但f5
的那個欄位就直接被當作字串,加上雙引號""
處理了,然而我們原本並不想要這樣。若要去掉雙引號,就還得再替f5
欄位建立一個型別來包裹字串,並替這個新型別實作Debug
特性,使其不會在格式化時加上雙引號,再把字串用新型別包裹後的結果丟給Formatter
結構實體提供的debug_*
方法建立出來的builder
來使用。似乎愈來愈麻煩了,事實上,我們可以使用以下介紹的套件來用一行程式敘述搞定所有的問題。
Debug Helper
「Debug Helper」是筆者開發的套件,提供了宣告式巨集,可以替結構體、元組結構體(tuple struct)、列舉等復合型別快速實作出Debug
特性。
Crates.io
Cargo.toml
使用方法
一開始要先引用debug_helper
這個crate下的巨集。
對於含有名稱欄位的結構體,可以使用impl_debug_for_struct!
巨集來實作Debug
特性。例如上面的例子,可以用impl_debug_for_struct!
巨集改寫如下:
use std::fmt::Display;
struct MyStruct {
f1: u8,
f2: i16,
f3: f64,
f4: String,
f5: Box<dyn Display>,
}
use std::fmt::{Debug, Formatter, Result as FormatResult};
impl Debug for MyStruct {
fn fmt(&self, f: &mut Formatter<'_>) -> FormatResult {
debug_helper::impl_debug_for_struct!(MyStruct, f, self, .f1, .f2, .f3, .f4, (.f5, "{}", self.f5))
}
}
fn main() {
let s = MyStruct {
f1: 1, f2: 2, f3: 3.0, f4: "4".to_string(), f5: Box::new("5")
};
println!("{s:05?}");
println!("{s:#05?}");
}
執行結果如下:
MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4", f5: 5 } MyStruct { f1: 00001, f2: 00002, f3: 003.0, f4: "4", f5: 5, }
如果是要對元組結構體實作Debug
特性,可以使用impl_debug_for_tuple_struct!
巨集,程式如下:
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub struct A(pub u8, pub i16, pub f64);
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_tuple_struct!(A, f, self, .0, (.2, "{:.3}", self.2));
}
}
let a = A(1, 2, std::f64::consts::PI);
println!("{:#?}", a);
/*
A(
1,
3.142,
)
*/
如果是要對列舉實作Debug
特性,可以使用impl_debug_for_enum!
巨集,程式如下:
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub enum A {
V1,
V2(u8, i16, f64),
V3 {
f1: u8,
f2: i16,
f3: f64,
},
}
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_enum!(A::{V1, (V2(f1, _, f3): (.f1, (.f3, "{:.3}", f3))), {V3{f1, f2: _, f3}: (.f1, (.f3, "{:.3}", f3))}}, f, self);
}
}
let a = A::V1;
let b = A::V2(1, 2, std::f64::consts::PI);
let c = A::V3{
f1: 1,
f2: 2,
f3: std::f64::consts::PI,
};
println!("{:#?}", a);
println!("{:#?}", b);
println!("{:#?}", c);
/*
V1
V2(
1,
3.142,
)
V3 {
f1: 1,
f3: 3.142,
}
*/
如果想要增加原本不存在的欄位,程式如下:
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub struct A {
pub f1: u8,
pub f2: i16,
pub f3: f64,
}
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_struct!(A, f, self, .f1, (.f3, "{:.3}", self.f3), (.sum, "{:.3}", self.f1 as f64 + self.f2 as f64 + self.f3));
}
}
let a = A {
f1: 1,
f2: 2,
f3: std::f64::consts::PI,
};
println!("{:#?}", a);
/*
A {
f1: 1,
f3: 3.142,
sum: 6.142,
}
*/
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub struct A(pub u8, pub i16, pub f64);
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_tuple_struct!(A, f, self, .0, (.2, "{:.3}", self.2), (.3, "{:.3}", self.0 as f64 + self.1 as f64 + self.2));
}
}
let a = A(1, 2, std::f64::consts::PI);
println!("{:#?}", a);
/*
A(
1,
3.142,
6.142,
)
*/
將元組結構體格式化成一般結構體:
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub struct A(pub u8, pub i16, pub f64);
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_struct!(A, f, self, let .f1 = self.0, let .f2 = self.1, let .f3 = self.2);
}
}
let a = A(1, 2, std::f64::consts::PI);
println!("{:#?}", a);
/*
A {
f1: 1,
f2: 2,
f3: 3.141592653589793,
}
*/
將一般結構體格式化成元組結構體:
#[macro_use]
extern crate debug_helper;
use std::fmt::{self, Formatter, Debug};
pub struct A {
pub f1: u8,
pub f2: i16,
pub f3: f64,
}
impl Debug for A {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
impl_debug_for_tuple_struct!(A, f, self, let .0 = self.f1, let .1 = self.f2, let .2 = self.f3);
}
}
let a = A {
f1: 1,
f2: 2,
f3: std::f64::consts::PI,
};
println!("{:#?}", a);
/*
A(
1,
2,
3.141592653589793,
)
*/