ys memos

Blog

Rustのstd::io::Writerを引数にする関数のwriteをStringに格納する方法


rust

2023/11/29


Rustでは、ターミナル出力などを主な用途とする関数やメソッドは、以下のように定義されている場合があると思う。

pub fn generate(buf: &mut dyn std::io::Write) {
  buf.write("Hello World!".as_bytes());
}

通常、このような実装には

fn main() {
    generate(&mut std::io::stdout());
}

のようにstd::io::stdout()を渡したり、std::net::TcpStreamのように予めWriteが実装されたものを渡し、任意の何かに書き込むのが通常の使い方と考えられるが、たまにその出力結果をStringに格納したいシチュエーションもあるだろう。

その時の解決方法を、軽くまとめておく。


generate()に閉ざされているHello World!をターミナル出力ではなく文字列で利用できるようにする。


必要最小限の情報を保持できるニュータイプ(構造体でも可)を作って、そこに必要なトレイトを実装する。


まずは課題を整理だが、上で挙げたgenerate(buf)bufの型はRustに慣れていないと難解に見えるが、言葉で説明すると、「std::io::Writeを実装した任意の型」と読み替えるとよい。

この問題を解決する上では、構造体かニュータイプのどちらかを選ぶことになる。ここでは値をいれて取り出せればいいので、ニュータイプのほうが簡潔に記述できるという観点から、ニュータイプを選んだ。


struct WritableString(String);

impl WritableString {
    pub fn new() -> Self {
        Self(String::new())
    }

    pub fn string(self) -> String {
        self.0
    }
}

impl std::io::Write for WritableString {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let s = std::str::from_utf8(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        self.0.push_str(s);
        Ok(s.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}


まずはstd::io::Writeの実装対象とする型をつくる。

struct WritableString(String);

ニュータイプの値へのアクセスは利用時には可読性低下の原因になりうるので、コンストラクタとアクセサを実装して扱いやすさを向上させる。

ここらへんの実装は好みで拡張することが推奨。メソッド名はなんでもいいし、fn new(s: String)のように初期値を指定させてもいいし、ゲッターで所有権を移動させないようにしても良いかもしれない。

impl WritableString {
    pub fn new() -> Self {
        Self(String::new())
    }

    pub fn string(self) -> String {
        self.0
    }
}

ここが問題の解決となる実装。これによって、サンプルとなっているgenerate()WritableStringを渡せるようになる。

write()Stringに文字を追加する処理とし、flush()は何もしないものとした。用途によっては文字列を空にする実装のほうがいい場合もありうる。

impl std::io::Write for WritableString {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let s = std::str::from_utf8(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        self.0.push_str(s);
        Ok(s.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

上で定義した generate()に対して、このように渡すことができる。

fn main() {
    let mut ws = WritableString::new();
    generate(&mut ws);
    println!("{}", ws.string());
}

pub fn generate(buf: &mut dyn std::io::Write)pub fn generate(buf: &mut impl std::io::Write)であっても同じ手法での解決が可能。


Rustでは、多くのことを気軽に拡張できる仕組みが充実しており、これらを駆使して柔軟にソフトウェア開発ができますね!


関連タグを探す