この記事は Rust Advent Calendar 2025 の15日目の記事です。
大遅刻、すみませんすみません。
Rust ではオリジナルの構造体を println!() で印字しようとしても、デフォルトでは印字できません。
// 二次元平面上の点を表現する構造体 struct Point { name: String, x: f32, y: f32, } fn main () { let a = Point { name: String::from("A"), x: 1.0, y: 2.0, }; println!("{:?}", a); // ^ `Point` cannot be formatted using `{:?}` // because it doesn't implement `Debug` }
{:?} で印字するには Debug トレイトが必要とのことで、derive 属性をつけて Debug を実装してあげれば印字可能になります。
// 二次元平面上の点を表現する構造体 + #[derive(Debug)] struct Point { name: String, x: f32, y: f32, } fn main () { let a = Point { name: String::from("A"), x: 1.0, y: 2.0, }; println!("{:?}", a); + // => Point { name: "A", x: 1.0, y: 2.0 } }
#[derive(Debug)] するだけで実装できるのはお手軽でいいですね。
独自の形式で印字する
もう一歩踏み込んで構造体の意味に合わせた独自の形式での印字をやってみましょう。
struct Point { name, x, y } は「x座標, y座標を指定した二次元平面上の点」を表現しています。なので、 Point { name: "A", x: 1.0, y: 2.0 } を印字するときには A (1, 2) としてみるとどうでしょうか。
Debug トレイトは独自に impl することも可能です。やってみましょう。
+ use std::fmt::{Debug, Formatter, Result}; // 二次元平面上の点を表現する構造体 - #[derive(Debug)] struct Point { name: String, x: f32, y: f32, } + impl Debug for Point { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "{} ({}, {})", self.name, self.x, self.y) + } + } fn main () { let a = Point { name: String::from("A"), x: 1.0, y: 2.0, }; println!("{:?}", a); - // => Point { name: "A", x: 1.0, y: 2.0 } + // => A (1, 2) }
impl Debug for Point で Debug::fmt メソッドを独自に実装しています。
やっていることは単純で、 write!() を呼び出して self.name, self.x, self.y をお好みの形式に印字しているだけです。
write!(f, "{} ({}, {})", self.name, self.x, self.y)
雰囲気としては、format!("{} ({}, {})", self.name, self.x, self.y) と同じことですね。
Debug トレイトは Formatter 構造体と密接に結びついている
ここまで読んで「つまり Debug::fmt メソッドは値を文字列化して返せばいいんだな」と思いませんでしたか?私は思ってました。
しかし println!() の結果をよく見てみると、同じ値に対してインデントが付与される場合があることに気づきます。
let piyo2 = vec!["piyo"; 2]; println!("{:?}", piyo2); // => ["piyo", "piyo"] println!("{:#?}", piyo2); // => [ // "piyo", // "piyo", // ]
Debug::fmt メソッドの結果がただの文字列であれば、このような整形はできないはずです。
write!() による実装はお手軽ですが、Debug トレイトでできることの全てではありません。
次からもっと凝った実装例を見ていきましょう。
List 表示, Set 表示
組み込みの構造体に std::fmt::DebugList というものがあります。これを使うと Vec を format!() したときのような表示をユーザーが手軽に再現できます。
use std::fmt::{Debug, Formatter, Result}; struct MyVec<'a, T> { items: &'a [T], } impl<'a, T: Debug> Debug for MyVec<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.debug_list() .entries(self.items.iter()) .finish() } } fn main () { let v = MyVec { items: &vec!["hoge", "fuga"] }; println!("{:?}", v); // => ["hoge", "fuga"] println!("{:#?}", v); // => [ // "hoge", // "fuga", // ] }
{:?} なら1行で、{:#?} なら複数行でインデントして印字されていますね。
実装は簡単です。 f.debug_list() が DebugList のインスタンスを返すのでこれを使います。
そのあと .entry(item) や .entries(items_iter) で要素を与えていくだけです。最後に .finish() すれば Result<(), std::fmt::Error> が得られるので、これをそのまま返します。
std::fmt::DebugList の仲間に std::fmt::DebugSet があります。
DebugList は [ ... ] 形式で印字してくれていました。DebugSet は { ... } 形式で値を印字してくれます。
Set 表示をしたいときには f.debug_set() を使います。
use std::fmt::{Debug, Formatter, Result}; struct MySet<'a, T> { items: &'a [T], } impl<'a, T: Debug> Debug for MySet<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.debug_set() .entries(self.items.iter()) .finish() } } fn main() { let s = MySet { items: &["hoge", "fuga"] }; println!("{:?}", s); // => {"hoge", "fuga"} println!("{:#?}", s); // => { // "hoge", // "fuga", // } }
構造体表示, タプル構造体表示
構造体 struct S { ... } やタプル構造体 struct T ( ... ) を format!() したときのような表示を再現することもできます。
std::fmt::DebugStruct, std::fmt::DebugTuple を使います。
use std::collections::HashMap; use std::fmt::{Debug, Formatter, Result}; struct MyStruct<'a, K, V> { name: String, items: &'a HashMap<K, V>, } impl<'a, K: Debug + Ord, V: Debug> Debug for MyStruct<'a, K, V> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut entries: Vec<_> = self.items.iter().collect(); entries.sort_by_key(|(k, _)| *k); let mut dbg = f.debug_struct(&self.name); for (ref k, ref v) in entries { dbg.field(&format!("{:?}", k), v); } dbg.finish() } } struct MyTuple<'a, T> { name: String, items: &'a [T], } impl<'a, T: Debug> Debug for MyTuple<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut dbg = f.debug_tuple(&self.name); for item in self.items.iter() { dbg.field(item); } dbg.finish() } } fn main() { let s = MyStruct { name: String::from("S"), items: &HashMap::from([("hoge", "fuga")]), }; println!("{:?}", s); // => S { "hoge": "fuga" } let t = MyTuple { name: String::from("T"), items: &["hoge", "fuga"], }; println!("{:?}", t); // => T("hoge", "fuga") }
構造体表示したい場合には f.debug_struct(name) からの .field(k, v) 、タプル構造体表示したい場合には f.debug_tuple(name) からの .field(v) するだけです。
応用例
これらの知識を使ってより高度な応用例を紹介します。
インデックス番号付きの List 表示
use std::fmt::{Debug, Formatter, Result}; struct IndexedList<'a, T>(&'a [T]); impl<'a, T: Debug> Debug for IndexedList<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut dbg = f.debug_list(); for (i, item) in self.0.iter().enumerate() { dbg.entry(&IndexedItem(&(i, item))); } dbg.finish() } } struct IndexedItem<'a, T>(&'a T); impl<'a, T: Debug> Debug for IndexedItem<'a, (usize, &'a T)> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(&self.0.0, f)?; write!(f, ": ")?; Debug::fmt(self.0.1, f) } } fn main() { let v = vec!["hoge", "fuga", "piyo"]; println!("{:?}", v); // => ["hoge", "fuga", "piyo"] println!("{:?}", IndexedList(&v)); // => [0: "hoge", 1: "fuga", 2: "piyo"] println!("{:#?}", IndexedList(&v)); // => [ // 0: "hoge", // 1: "fuga", // 2: "piyo", // ] }
いつもの List 表示っぽくもありつつ、インデックス番号を添えてくれるのでどの要素が何番目かわかりやすくなります。
末尾要素を省略した List 表示
use std::fmt::{Debug, Formatter, Result}; struct EllipsisList<'a, T>(&'a [T]); impl<'a, T: Debug> Debug for EllipsisList<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut dbg = f.debug_list(); let mut iter = self.0.iter(); // 高々3要素まで表示し、残りがあれば省略表示する for _ in 0..3 { if let Some(item) = iter.next() { dbg.entry(item); } else { break; } } if iter.next().is_some() { dbg.entry(&Ellipsis); } dbg.finish() } } struct Ellipsis; impl Debug for Ellipsis { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "...") } } fn main() { let v = vec![0, 1, 2, 3, 4, 5]; println!("{:?}", v); // => [0, 1, 2, 3, 4, 5] println!("{:?}", EllipsisList(&v)); // => [0, 1, 2, ...] println!("{:#?}", EllipsisList(&v)); // => [ // 0, // 1, // 2, // ..., // ] }
長い List を全部印字する必要がなく、先頭要素だけに興味がある場合はこのような方法も便利ですね。
ポイントは ... を印字するのに dbg.entry("..."); とするのではなく、ユニット構造体 Ellipsis を定義して dbg.entry(&Ellipsis); としているところです。
&str も dyn Debug なので dbg.entry("..."); と書くことは可能ですが、これだと印字結果は "..." となり、余計なダブルクォートがついてしまいます。
まとめ
Rust の Debug トレイトは #[derive(Debug)] や write!() でお手軽に使うこともできるし、カスタマイズしてこだわった表示にもできるのが魅力的ですね。
私からは以上です。