Debug トレイトの derive 属性での実装を見てみよう
前回は trybuild crate で derive macro のテストをしました。今回は基本に立ち返って Debug trait を見てみます。特に derive 属性での実装を見ていこうと思います。
Debug トレイトとは
Debug はデバッグ出力のフォーマットができることを表すトレイトです。 std::fmt::Debug で定義されています。
https://doc.rust-lang.org/std/fmt/trait.Debug.html
要するに format!("{:?}", o) できるということですね。 format!("{:#?}", o) もできます。 {:#?} は pritty-print です。
ほとんどのケースでは struct や enum に #[derive(Debug)] とつけることで実装します。たとえば、すべてのフィールドが出力に含まれるとまずいケースなどで手動での実装を選択することになると思います。
一応ドキュメントには derive で実装した際に struct の場合はこう出力され、 enum の場合こう出力される……のようなことが書いてあるものの、 not stable とも書いてあるので、デバッグ出力の形式に依存するようなことはやめたほうが良さそうです。
提供されるメソッドは fmt のみ。これは Display trait ともよく似ていますね。 Display trait については弊社の開発メンバーが書いていましたね。
derive 属性での実装
#[derive(Debug)] で実装したときの動作を調べていきます。
ざっと動作を確認し、 cargo expand してみて、それぞれの型の結果を見ていきます。
ざっと動作を確認
#[derive(Debug)]
struct Unit;
#[derive(Debug)]
struct Tuple1(());
#[derive(Debug)]
struct Tuple2(bool, i32);
#[derive(Debug)]
struct Struct1 {
s: String,
}
#[derive(Debug)]
struct Struct2 {
nested: Struct1,
}
#[derive(Debug)]
enum Enum {
Unit,
Tuple(bool),
Struct { n: i32 },
}
fn main() {
let s = Unit;
assert_eq!(format!("{:?}", s), "Unit");
let s = Tuple1(());
assert_eq!(format!("{:?}", s), "Tuple1(())");
let s = Tuple2(true, 123);
assert_eq!(format!("{:?}", s), "Tuple2(true, 123)");
let s = Struct1 {
s: "abc".to_owned(),
};
assert_eq!(format!("{:?}", s), "Struct1 { s: \"abc\" }");
let s = Struct2 { nested: s };
assert_eq!(
format!("{:?}", s),
"Struct2 { nested: Struct1 { s: \"abc\" } }"
);
let s = Enum::Unit;
assert_eq!(format!("{:?}", s), "Unit");
let s = Enum::Tuple(true);
assert_eq!(format!("{:?}", s), "Tuple(true)");
let s = Enum::Struct { n: 123 };
assert_eq!(format!("{:?}", s), "Struct { n: 123 }");
}
cargo expand
以下は前述のコードを cargo expand した結果から抜粋したもの。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
struct Unit;
#[automatically_derived]
impl ::core::fmt::Debug for Unit {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f, "Unit")
}
}
struct Tuple1(());
#[automatically_derived]
impl ::core::fmt::Debug for Tuple1 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Tuple1", &&self.0)
}
}
struct Tuple2(bool, i32);
#[automatically_derived]
impl ::core::fmt::Debug for Tuple2 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field2_finish(f, "Tuple2", &self.0, &&self.1)
}
}
struct Struct1 {
s: String,
}
#[automatically_derived]
impl ::core::fmt::Debug for Struct1 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "Struct1", "s", &&self.s)
}
}
struct Struct2 {
nested: Struct1,
}
#[automatically_derived]
impl ::core::fmt::Debug for Struct2 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(
f,
"Struct2",
"nested",
&&self.nested,
)
}
}
enum Enum {
Unit,
Tuple(bool),
Struct { n: i32 },
}
#[automatically_derived]
impl ::core::fmt::Debug for Enum {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Enum::Unit => ::core::fmt::Formatter::write_str(f, "Unit"),
Enum::Tuple(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Tuple", &__self_0)
}
Enum::Struct { n: __self_0 } => {
::core::fmt::Formatter::debug_struct_field1_finish(
f,
"Struct",
"n",
&__self_0,
)
}
}
}
}
unit struct
それぞれの詳細を見ていきます。
#[derive(Debug)]
struct Unit;
struct Unit;
#[automatically_derived]
impl ::core::fmt::Debug for Unit {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f, "Unit")
}
}
write_str で識別子を返すのみで、特に言うことはないですね。
tuple struct
#[derive(Debug)]
struct Tuple1(());
#[derive(Debug)]
struct Tuple2(bool, i32);
struct Tuple1(());
#[automatically_derived]
impl ::core::fmt::Debug for Tuple1 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Tuple1", &&self.0)
}
}
struct Tuple2(bool, i32);
#[automatically_derived]
impl ::core::fmt::Debug for Tuple2 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field2_finish(f, "Tuple2", &self.0, &&self.1)
}
}
debug_tuple_fieldN_finish(f, 識別子, フィールド, ...) を使っています。
debug_tuple_fieldN_finish は #[doc(hidden)] が設定された関数みたいです。ドキュメントにないということはおそらく内部的なもので直接使うべきものではないのでしょう。
https://doc.rust-lang.org/src/core/fmt/mod.rs.html#2394-2403
/// Shrinks `derive(Debug)` code, for faster compilation and smaller
/// binaries. `debug_tuple_fields_finish` is more general, but this is faster
/// for 1 field.
#[doc(hidden)]
#[unstable(feature = "fmt_helpers_for_derive", issue = "none")]
pub fn debug_tuple_field1_finish<'b>(&'b mut self, name: &str, value1: &dyn Debug) -> Result {
let mut builder = builders::debug_tuple_new(self, name);
builder.field(value1);
builder.finish()
}
builders::debug_tuple_new は Formatter::debug_tuple の実装と同じものです。特別な動きがあるわけでもありません。
https://doc.rust-lang.org/src/core/fmt/mod.rs.html#2390
pub fn debug_tuple<'b>(&'b mut self, name: &str) -> DebugTuple<'b, 'a> {
builders::debug_tuple_new(self, name)
}
全体としてはコメントにあるとおり、高速化などの観点から Formatter の組み立てを簡素化しているようです。
struct
#[derive(Debug)]
struct Struct1 {
s: String,
}
#[derive(Debug)]
struct Struct2 {
nested: Struct1,
}
struct Struct1 {
s: String,
}
#[automatically_derived]
impl ::core::fmt::Debug for Struct1 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f, "Struct1", "s", &&self.s)
}
}
struct Struct2 {
nested: Struct1,
}
#[automatically_derived]
impl ::core::fmt::Debug for Struct2 {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(
f,
"Struct2",
"nested",
&&self.nested,
)
}
}
内部的には tuple struct と大差なく、 debug_struct_fieldN_finish を使用している (引数にフィールド名が増えている) くらいです。
https://doc.rust-lang.org/src/core/fmt/mod.rs.html#2235
/// Shrinks `derive(Debug)` code, for faster compilation and smaller
/// binaries. `debug_struct_fields_finish` is more general, but this is
/// faster for 1 field.
#[doc(hidden)]
#[unstable(feature = "fmt_helpers_for_derive", issue = "none")]
pub fn debug_struct_field1_finish<'b>(
&'b mut self,
name: &str,
name1: &str,
value1: &dyn Debug,
) -> Result {
let mut builder = builders::debug_struct_new(self, name);
builder.field(name1, value1);
builder.finish()
}
name が増えるくらいでほとんど同じものです。
builders::debug_struct_new は Formatter::debug_struct の実装と同じものですね。このあたりも tuple struct とほぼ同じです。
https://doc.rust-lang.org/src/core/fmt/mod.rs.html#2231
pub fn debug_struct<'b>(&'b mut self, name: &str) -> DebugStruct<'b, 'a> {
builders::debug_struct_new(self, name)
}
enum
最後は enum です。
#[derive(Debug)]
enum Enum {
Unit,
Tuple(bool),
Struct { n: i32 },
}
enum Enum {
Unit,
Tuple(bool),
Struct { n: i32 },
}
#[automatically_derived]
impl ::core::fmt::Debug for Enum {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
Enum::Unit => ::core::fmt::Formatter::write_str(f, "Unit"),
Enum::Tuple(__self_0) => {
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Tuple", &__self_0)
}
Enum::Struct { n: __self_0 } => {
::core::fmt::Formatter::debug_struct_field1_finish(
f,
"Struct",
"n",
&__self_0,
)
}
}
}
}
match で分岐し、型名の代わりに variant が使われているものの、あとは struct と同じですね。
おわりに
本当は「 derive の実装」に続けて「手動での実装」を書くつもりだったのですが、長くなってしまうので次回にします。
次回は std::fmt::Formatter やそれを使った手動での Debug トレイトの実装を見ていきます。
参考
- Debug in std::fmt - Rust https://doc.rust-lang.org/std/fmt/trait.Debug.html
Discussion