🔨

Debug トレイトの手動実装

に公開

前回は Debug トレイトの derive 属性での実装を確認しました

今回は前回の続きなのですが、 Debug トレイトを手動で実装していきます。

いつ Debug トレイトは手動実装する?

前回書いたとおり、ほとんどの場合に Debug トレイトは derive 属性で実装できます。では、いつ、手動で実装するのでしょう?

いくつかの例を挙げます。

  • Debug ではないフィールドがあるとき
  • 出力したくないフィールドがあるとき
  • thiserror の transparent のような挙動にしたいとき

Debug ではないフィールドがあるとき

Debug でないフィールドを含む場合は derive 属性での実装はできません。

struct NonDebug;

#[derive(Debug)]
struct MyDebug {
    field: NonDebug,
}
error[E0277]: `NonDebug` doesn't implement `Debug`
 --> tests/ui/compile_fail_non_debug_field.rs:5:5
  |
3 | #[derive(Debug)]
  |          ----- in this derive macro expansion
4 | struct MyDebug {
5 |     field: NonDebug,
  |     ^^^^^^^^^^^^^^^ `NonDebug` cannot be formatted using `{:?}`
  |
  = help: the trait `Debug` is not implemented for `NonDebug`
  = note: add `#[derive(Debug)]` to `NonDebug` or manually `impl Debug for NonDebug`
help: consider annotating `NonDebug` with `#[derive(Debug)]`
  |
1 + #[derive(Debug)]
2 | struct NonDebug;
  |

こういうときは手動で Debug トレイトを実装し、 Debug でないフィールドはダミーの文字列を使うなどして回避できます。

struct NonDebug;

struct MyDebug {
    field: NonDebug,
}

impl std::fmt::Debug for MyDebug {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MyDebug")
            .field("field", &"NonDebug")
            .finish()
    }
}

fn main() {
    assert_eq!(
        format!("{:?}", MyDebug { field: NonDebug }),
        "MyDebug { field: \"NonDebug\" }"
    );
}

出力したくないフィールドがあるとき

出力したくないフィールドがある場合は Debug トレイトを手動実装する状況としてあります。

たとえば、ユーザーの入力したパスワードやトークンなどは、そのままログなどにデバッグ出力してしまうと、セキュリティ上の問題があります。

しかし、 derive 属性で実装するとデバッグ出力に含まれてしまいます。

#[derive(Debug)]
struct Input1 {
    email: String,
    password: String,
}
assert_eq!(
    format!(
        "{:?}",
        Input1 {
            email: "happy@example.com".to_owned(),
            password: "password".to_owned()
        }
    ),
    "Input1 { email: \"happy@example.com\", password: \"password\" }"
);

手動実装であれば、出力したくないフィールドを外したり、マスクするなどの対応ができます。

struct Input2 {
    email: String,
    password: String,
}

impl std::fmt::Debug for Input2 {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Input2")
            .field("email", &self.email)
            .field("password", &"********")
            .finish()
    }
}

assert_eq!(
    format!(
        "{:?}",
        Input2 {
            email: "happy@example.com".to_owned(),
            password: "password".to_owned()
        }
    ),
    "Input2 { email: \"happy@example.com\", password: \"********\" }"
);

thiserror の transparent のような挙動にしたいとき

最後は独自の trait の実装などのために既存の型を wrap した場合 (newtype パターン) です。

素朴に derive 属性で実装すると、 Id(Id(...)) のように入れ子であることが分かる形で出力されます。実害はないかもしれませんが、 Id(...) のように内側の型を透過的に出力できるほうが嬉しいこともあるでしょう。

mod other {
    #[derive(Debug)]
    pub struct Id(pub String);
}

mod example1 {
    #[derive(Debug)]
    pub struct Id(pub super::other::Id);
}

mod example2 {
    pub struct Id(pub super::other::Id);
    impl std::fmt::Debug for Id {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            std::fmt::Debug::fmt(&self.0, f)
        }
    }
}

fn main() {
    assert_eq!(
        format!("{:?}", other::Id("abc123".to_owned())),
        "Id(\"abc123\")"
    );

    assert_eq!(
        format!("{:?}", example1::Id(other::Id("abc123".to_owned()))),
        "Id(Id(\"abc123\"))"
    );

    assert_eq!(
        format!("{:?}", example2::Id(other::Id("abc123".to_owned()))),
        "Id(\"abc123\")"
    );
}

もっといろいろな形式で出力したい

既に説明なく使っていますが、 fmt の引数に渡される std::fmt::Formatter を使うと、いろいろな形式で出力できます。

デバッグ出力を用意に組み立てるためのメソッドとして、↓のようなものが提供されています。

  • debug_list
  • debug_map
  • debug_set
  • debug_struct
  • debug_tuple

これらでビルダーを生成し、 fieldentry などでフィールドやエントリを追加して、 finishfinish_non_exhaustive で出力を終えます。

そういった構造に寄せる必要がないなら f.write_str() で任意の文字列形式を出力できます。

リストやマップでないものも、それのデバッグ出力と同様の形式で出力できます。

struct MyList(i32, i32);

impl std::fmt::Debug for MyList {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_list().entry(&self.0).entry(&self.1).finish()
    }
}

struct MyMap(i32, i32);

impl std::fmt::Debug for MyMap {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_map().entry(&self.0, &self.1).finish()
    }
}

struct MySet(i32, i32);

impl std::fmt::Debug for MySet {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_set().entry(&self.0).entry(&self.1).finish()
    }
}

assert_eq!(format!("{:?}", MyList(1, 2)), "[1, 2]");
assert_eq!(format!("{:?}", MyMap(1, 2)), "{1: 2}");
assert_eq!(format!("{:?}", MySet(1, 2)), "{1, 2}");

おわりに

今回は Debug トレイトの手動実装する状況や std::fmt::Formatter の使い方について書きました。

次回は axum crate について書こうと思います。

参考

GitHubで編集を提案
ドクターメイト

Discussion