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:可以傳入一個字串切片,並且可以根據widthprecisionfill的資訊對其做「填充」(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 = "*"

使用方法

一開始要先引用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,
    )
*/