Tomoki Ota's Blog

article icon

【Rust】std::fmt

作成日 

はじめに

この記事では、std::fmtについて解説します。まず、[std::fmt]に実装されているprint!write!などの標準出力/入力に関わるマクロについて説明します。その後、トレイトについても詳しく掘り下げていきます。

マクロ

println!などはstd::fmtのマクロである。 println!やwriteln!は、printやwriteに改行を加えただけなので飛ばします。また、eprintなどもprint!、write!を理解できれば、あとは標準エラー出力 (stderr)になるだけなので説明を省きます。

print

macro_rules! print {
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args!($($arg)*));
    }};
}

これらはformat_args!というマクロを介してプロクシされる。print!などの派生マクロとは異なり、ヒープ割り当てを回避するcore::fmtに定義されたマクロである。format_args!自体は文字列を返すのではなく、fmt::Argumentsを返す。そしてstd::io::_print();に渡して、文字列に変換し標準出力する。

write

write!でもformta_args!()fmt::Argumetnsを返し、write_fmt()というメソッドに渡している。

macro_rules! write {
    ($dst:expr, $($arg:tt)*) => {
        $dst.write_fmt($crate::format_args!($($arg)*))
    };
}

write_fmt()はcore::fmtのWriteというトレイトのメソッドである。

fmt::Arguments

core:fmtにはfmt::Argumentsが実装されている。実装は以下のようになっている。

pub struct Arguments<'a> { 
    pieces: &'a [&'static str],
    fmt: Option<&'a [rt::Placeholder]>,
    args: &'a [rt::Argument<'a>],
}
ライフタイム

Arguments<'a><'a>はライフタイムと呼ばれる。Arguments 構造体が内部にライフタイム'aに関連する参照を保持していることを示している。この構造体のデータはライフタイム'aが有効な間だけ使うことができる。

トレイト

fmt::coreのトレイトで重要なトレイトが3つある。

Displayトレイト

pub trait Display {
    #[doc = include_str!("fmt_trait_method_doc.md")]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

fmt::ArgumentsDisplayの振る舞いを実装している。

#[stable(feature = "rust1", since = "1.0.0")]
impl Debug for Arguments<'_> {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
        Display::fmt(self, fmt)
    }
}
 
#[stable(feature = "rust1", since = "1.0.0")]
impl Display for Arguments<'_> {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
        write(fmt.buf, *self)
    }
}

fmtメソッドを実装して、Displayトレイトを満たしていれば、println!でコンパイラはDisplayトレイトを実装しているか確認します。

use std::fmt;
 
struct Person {
    name: String,
    age: u8,
}
 
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Name: {}, age: {}", self.name, self.age)
    }
}
 
fn main() {
    let person = Person {
        name: String::from("Taro"),
        age: 20,
    };
    println!("{}", person); // Name: Taro, age: 20
}

もちろんDisplayトレイトを実装していなければ、コンパイルエラーとなります。

struct Person {
    name: String,
    age: u8,
}
 
fn main() {
    let person = Person {
        name: String::from("Taro"),
        age: 20,
    };
    println!("{}", person); // コンパイルエラー:`Person` doesn't implement `std::fmt::Display`
}```
 
### Debugトレイト
 
Debugトレイトの実装は以下のようになっています。
 
```rust
pub trait Debug {
    #[doc = include_str!("fmt_trait_method_doc.md")]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

fmtメソッドを実装して、Debugトレイトを満たしていれば、println!などの中で:?とすることでコンパイラはDebugトレイトを実装しているか確認します。(:#?も同様だが、整形(pritty-print)した形式となる。)

use std::fmt;
 
struct Person {
    name: String,
    age: u8,
}
 
impl fmt::Debug for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Person {{ name: \"{}\", age: {} }}", self.name, self.age)
    }
}
 
fn main() {
    let person = Person {
        name: String::from("Taro"),
        age: 20,
    };
 
    println!("{:?}", person); // Person { name: "Taro", age: 20 }
}

同じようにfmt::ArgumentsはDebugの振る舞いを実装していることが分かります。

#[stable(feature = "rust1", since = "1.0.0")]
impl Debug for Arguments<'_> {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
        Display::fmt(self, fmt)
    }
}

ちなみに、#[derive(Debug)]を使用すると、自動でDebugのトレイトの振る舞いを実装してくれる。 先ほどの例は下記のようにもかける。

# [derive(Debug)]
struct Person {
    name: String,
    age: u8,
}
 
fn main() {
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };
 
    println!("{:?}", person);
}

Writeトレイト

先述した通りWriteトレイトでwrite_fmt()が実装されている。

pub trait Write {
    #[stable(feature = "fmt_write_char", since = "1.1.0")]
    fn write_char(&mut self, c: char) -> Result {
        self.write_str(c.encode_utf8(&mut [0; 4]))
    }
 
    #[stable(feature = "rust1", since = "1.0.0")]
    fn write_fmt(&mut self, args: Arguments<'_>) -> Result {
        trait SpecWriteFmt {
            fn spec_write_fmt(self, args: Arguments<'_>) -> Result;
        }
 
        impl<W: Write + ?Sized> SpecWriteFmt for &mut W {
            #[inline]
            default fn spec_write_fmt(mut self, args: Arguments<'_>) -> Result {
                if let Some(s) = args.as_statically_known_str() {
                    self.write_str(s)
                } else {
                    write(&mut self, args)
                }
            }
        }
 
        impl<W: Write> SpecWriteFmt for &mut W {
            #[inline]
            fn spec_write_fmt(self, args: Arguments<'_>) -> Result {
                if let Some(s) = args.as_statically_known_str() {
                    self.write_str(s)
                } else {
                    write(self, args)
                }
            }
        }
 
        self.spec_write_fmt(args)
    }
}

write()マクロでは、以下のような実装になっていたが、$dstWriteの振る舞いを満たすのか疑問になった人もいると思います。

macro_rules! write {
    ($dst:expr, $($arg:tt)*) => {
        $dst.write_fmt($crate::format_args!($($arg)*))
    };
}

$dstWriteの振る舞いを満たす型でなければなりません。 そこでstd::Stringの実装を見てみると、Stringはfmt::Writeの振る舞いを満たしていることがわかります。

#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]
impl fmt::Write for String {
    #[inline]
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.push_str(s);
        Ok(())
    }
 
    #[inline]
    fn write_char(&mut self, c: char) -> fmt::Result {
        self.push(c);
        Ok(())
    }
}

なので、以下のようにString型だとwrite!で用いることができますが、&strのような型はStringなどのWriteの振る舞いを満たす型に変換する必要があります。

fn main() {
    let mut s = String::new();
 
    match write!(&mut s, "Hello World!") {
        Ok(_) => {
            let result: &str = &s;
            println!("{}", result);
        }
        Err(e) => {
            eprintln!("Error writing to string: {}", e);
        }
    }
}
なぜ&strにWriteトレイトを実装できないのか

Rustでは&stri32などのプリミティブ型にも、トレイトを実装することができます。

trait DoubleValue {
    fn double(&self) -> i32;
}
 
impl DoubleValue for i32 {
    fn double(&self) -> i32 {
        self * 2
    }
}
 
fn main() {
    let number: i32 = 10;
    println!("{} x 2 = {}", number, number.double());
}

しかし、Rustには孤児ルール(Orphan Rule)というものが存在します。孤児ルールとは、一貫性(Coherence)を満たすための規則です。 外部のトレイトを外部の型に対して実装することはできません。つまり、自分が定義していない型に対して、自分が定義していないトレイトを実装することはできません。この規則により、他人のコードが自分のコードを壊したり、 自分のコードが他人のコードを壊したりしないことを保証してくれます。この制約がなければ、2つのクレートで同じ型に対して同じトレイトを実装できてしまい、 コンパイラはどちらの実装を使うべきか分からなくなってしまいます。

  • 外部型 × 外部トレイト → ❌ 不可
  • 外部型 × 自作トレイト → ✅ 可能
  • 自作型 × 外部トレイト → ✅ 可能
  • 自作型 × 自作トレイト → ✅ 可能

もちろん&strなどイミュータブルであることもトレイトが実装できない理由の1つとなります。std::fmt::Writeトレイトは内部的にデータを書き換える必要があるので、イミュータブルな型で実装しても意味がない。

std::fmt::Writeトレイトのシグネチャをみると以下のようにミュータブルな参照を前提としています。

pub trait Write {
    fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error>;
}

したがって、イミュータブルな型に対してはWriteトレイトの振る舞いを提供できないことが分かりました。 ただし、最も根本的な原因は孤児ルールの存在であることを覚えていてください。

この記事をシェアするx icon
アイコン画像
Tomoki Ota

フルスタックエンジニア。Goが好き。趣味はカメラと旅行です📷