概要
構造体などのフィールドをフォーマットするときに、println!
マクロなどで与えられたフォーマット指定をそのまま使ってフォーマットしたいという状況にあるとします。このとき、フィールドのDisplay
トレイトのfmt
メソッドにFormatter
を渡して呼び出すことで、与えられたフォーマット指定を使って値をフォーマットすることができます。つまり、次のような形で実装します。
impl fmt::Display for MyData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.field.fmt(f)
}
}
はじめに
数値型をフィールドに持つ構造体にDisplay
トレイトを実装するときに、パディングや精度などのフォーマット指定を受け付けられるようにしたいと思います。これを実現するためには、フィールドのfmt
メソッドを呼び出せば良いということを、具体例を使って紹介します。
Display
トレイトの実装例はRust By Example 日本語版でも紹介されていますが1、これらの例はwrite!
などのマクロを呼び出す際に、Display
トレイトの実装の中でフォーマット文字列を新たに指定しているため、自ら実装したfmt
メソッドに与えられたフォーマット指定は無視されてしまいます。
表題の目的を実現する方法について書いている記事は、少なくとも日本語では見当たらなかったのでこの記事を書くことにしました。なお、この記事のソースコードはRust 1.48.0で動作を確認しました。
例
平面上の点を表現する構造体Point
を定義したとしましょう。
struct Point {
x: f64,
y: f64,
}
これにDisplay
トレイトを実装して、次のコードが通るようにしたいのです。
let pt = Point { x: 0.5, y: (3.0_f64).sqrt()/2.0 };
assert_eq!(format!("{}", pt), "(0.5, 0.8660254037844386)".to_owned());
assert_eq!(format!("{:.6}", pt), "(0.500000, 0.866025)".to_owned());
assert_eq!(format!("{:.prec$}", pt, prec=3), "(0.500, 0.866)".to_owned());
assert_eq!(format!("{:>8.4}", pt), "( 0.5000, 0.8660)".to_owned());
まずは素朴にDisplay
トレイトを実装してみましょう。
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
このソースコードを実行してみると以下の結果が得られます。
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 1.23s
Running `target/debug/playground`
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"(0.5, 0.8660254037844386)"`,
right: `"(0.500000, 0.866025)"`', src/main.rs:16:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
どうやら2番目のassert_eq!
でのフォーマット指定が無視されているようです。どうしてこうなったのでしょう?それは、今実装したDisplay
トレイトではwrite!
マクロでのフォーマット指定を{}
としたので、デフォルトのフォーマット方法で出力されたからです。
write!
マクロはWrite
トレイトのwrite_fmt
メソッドを呼び出します。write_fmt
メソッドはstd::fmt::write
関数を呼び出します。そして、write
関数は新しくFormatter
を作り、それを使って値をフォーマットします。
今、我々に与えられたFormatter
は、Point
構造体のfmt
メソッドの引数であるf
です。これを使うようにDisplay
トレイトの実装を修正しましょう。各フィールドのfmt
メソッドにf
を渡して呼べばOKです。少し冗長ですが、修正した実装は以下のようになります。
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(")?;
self.x.fmt(f)?;
write!(f, ", ")?;
self.y.fmt(f)?;
write!(f, ")")
}
}
このソースコードを実行してみましょう。パニックせずに終了します。これで、自分で実装したDisplay
トレイトで与えられたフォーマット指定を使うことができました。