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!
をターミナル出力ではなく文字列で利用できるようにする。
TL;DR
必要最小限の情報を保持できるニュータイプ(構造体でも可)を作って、そこに必要なトレイトを実装する。
方法
まずは課題を整理だが、上で挙げた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
}
}
Writeの実装
ここが問題の解決となる実装。これによって、サンプルとなっている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では、多くのことを気軽に拡張できる仕組みが充実しており、これらを駆使して柔軟にソフトウェア開発ができますね!