rust

Rustでファイルの入出力

この記事はWanoアドベントカレンダーの3日目の記事です。

TL;DR

  • どんなAPIがあるの?
    • std::fs
      • ファイルオープンやディレクトリ操作
    • std::io
      • IO関連のtrait(Read/Write等)や標準入出力
  • 注意事項
    • 標準の入出力APIのうち最もレイヤが低いAPIはバッファリングさえも除かれている
      • この事を把握せずに自分で頑張ろうとすると性能が出ない事が多い
      • BufReader/BufWriterを使えばバッファリングされるので基本はこれを使う
  • ユースケース別やり方
    • ファイルの内容全体を文字列として一気に読み込み
      • std::fs::read_to_stringを使う
      • std::io::Read traitのread_to_stringメソッドを使う
    • 1行ずつ文字列を読み込みたい
      • std::io::BufRead traitのread_lineメソッドかlinesメソッドを使う
      • BufRead traitはstd::io::BufReader型が実装している
    • ファイルの内容全体をバイト列として一気に読み込み
      • std::io::Read traitのread_to_endメソッドを使う
    • バイト列をバッファに読み込みながら処理したい
      • std::io::BufReaderを使う
      • BufReader相当の事を自分でやるならstd::io::Read traitのreadメソッドを使う
    • 面倒な事は一切しないで1バイトずつ処理したい
      • std::io::Read traitのbytesメソッドを使う
    • 文字列/バイト列を一度に書き出す
      • 自分でString型やVec<u8>型の変数に溜めてからstd::io::Write traitのwrite_allメソッドで一度に書き出す
      • 文字列の場合はwrite!もしくはwriteln!マクロがprintln!の様に使う事ができる。
    • 文字列/バイト列を少しずつ複数回に分けて書き出す
      • std::io::BufWriterを使う事以外は一度に書き出す場合と同じ

どんなAPIがあるの?

この2つが標準の同期APIです。

std::ioにはRead/Write/Seek/BufReadといった入出力の為のtraitや標準入出力の型(Stdin/Stdout/Stderr)等があります。

std::fsにはFile操作の為のFile型やディレクトリ操作の為の関数や型等があります。

注意事項

Read traitや Write traitの最もプリミティブな操作であるreadwriteメソッドは多くの場合に実装が低レイヤー過ぎる為に、他の言語と同様の感覚で使用するとパフォーマンスが出ないという問題が起こります。

ReadWrite traitを実装している型(例えばFileStdinStdout)を、BufReaderBufWriter型で包んであげる事でreadwriteでバッファリングが行われるようになり、他の言語と同様のパフォーマンスを出す事が出来るようになります。

ユースケース別やり方

ファイルの内容全体を文字列として一気に読み込み

Rust v1.26.0以降に追加されたstd::fs::read_to_stringを使用すると簡単です。

use std::fs;

fn main() -> Result<(), Box<std::error::Error>> {
    let content = fs::read_to_string("path/to/file")?;
    println!("{}", content);
    Ok(())
}

他にはRead traitのread_to_stringメソッドを使ったやり方もあります。

use std::io::{self, Read};

fn get_content<R: Read>(r: &mut R) -> io::Result<String> {
    let mut buf = String::new();
    let _ = r.read_to_string(&mut buf)?;
    Ok(buf)
}

fn main() -> Result<(), Box<std::error::Error>> {
    let content = get_content(&mut io::stdin())?;
    println!("{}", content);
    Ok(())
}

read_to_stringは引数に読み込んだデータを保存するバッファを要求するので、1つのString変数を使い回すことでメモリアロケーション回数を減らす事も出来ます。

1行ずつ文字列を読み込みたい

BufReaderが実装しているBufRead traitのlinesメソッドを使ってIteratorとして処理します。

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    for result in BufReader::new(File::open("path/to/file")?).lines() {
        let l = result?;
        println!("{}", l);
    }
    Ok(())
}

BufReaderが実装しているBufRead traitのread_lineメソッドを使って細かく制御していく事も出来ます。

use std::io::{self, BufRead, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    let mut reader = BufReader::new(io::stdin());
    let mut buf = String::new();

    while reader.read_line(&mut buf)? > 0 {
        println!("{}", buf);
        buf.clear();
    }

    Ok(())
}

read_lineを用いると改行文字もbufに入るので注意して下さい。

ファイルの内容全体をバイト列として一気に読み込み

Read traitのread_to_endメソッドを使います。

use std::fs::File;
use std::io::Read;

fn main() -> Result<(), Box<std::error::Error>> {
    let mut file = File::open("path/to/file")?;
    let mut buf = Vec::new();
    let _ = file.read_to_end(&mut buf)?;
    println!("{:?}", buf);
    Ok(())
}

バイト列をバッファに読み込みながら処理したい

BufReaderを使って読み込みます。

use std::fs::File;
use std::io::{Read, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    let mut reader = BufReader::new(File::open("path/to/file")?);
    let mut buf = [0; 4];

    loop {
        match reader.read(&mut buf)? {
            0 => break,
            n => {
                let buf = &buf[..n];
                println!("{:?}", buf);
            }
        }
    }

    Ok(())
}

何らかの理由でBufReaderのバッファリングが邪魔な場合はRead traitのreadメソッドを自分で呼び出して読み込んでいきます。

use std::fs::File;
use std::io::Read;

fn main() -> Result<(), Box<std::error::Error>> {
    let mut file = File::open("path/to/file")?;
    let mut buf = [0; 4];

    loop {
        match file.read(&mut buf)? {
            0 => break,
            n => {
                let buf = &buf[..n];
                println!("{:?}", buf);
            }
        }
    }

    Ok(())
}

面倒な事は一切しないで1バイトずつ処理したい

BufReaderを通してRead traitのbytesメソッドを呼びます。

use std::fs::File;
use std::io::{Read, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    for result in BufReader::new(File::open("path/to/file")?).bytes() {
        let byte = result?;
        println!("{:x}", byte);
    }

    Ok(())
}

文字列/バイト列を一度に書き出す

自分でバッファリングしてWrite traitのwrite_allメソッドを使って書き出します。

書き込み可能なファイルハンドルはFile型のcreateメソッドで作成します。

use std::fs::File;
use std::io::{self, Read, Write, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    let mut file = File::create("path/to/file")?;
    let buf = BufReader::new(io::stdin()).bytes().collect::<io::Result<Vec<u8>>>()?;
    file.write_all(&buf)?;
    file.flush()?;
    Ok(())
}

バイト列ではなく文字列の場合はwrite!またはwriteln!マクロを使う事でprintln!と同じ様に書きだす事ができます。

use std::fs::File;
use std::io::{self, BufRead, Write, BufReader};

fn main() -> Result<(), Box<std::error::Error>> {
    let mut file = File::create("path/to/file")?;
    let buf = BufReader::new(io::stdin()).lines().collect::<io::Result<Vec<String>>>()?.join("\n");
    write!(file, "{}", buf)?;
    file.flush()?;
    Ok(())
}

文字列/バイト列を少しずつ複数回に分けて書き出す

BufWriterを使う事以外は一度に書き出す場合と基本は同じです。

use std::fs::File;
use std::io::{self, Read, Write, BufReader, BufWriter};

fn main() -> Result<(), Box<std::error::Error>> {
    let mut writer = BufWriter::new(File::create("path/to/file")?);
    for result in BufReader::new(io::stdin()).bytes() {
        let byte = result?;
        writer.write_all(&[byte])?;
    }
    file.flush()?;
    Ok(())
}