Rustのファイル入出力
ファイルの読み書きは、これまで学んできた Result や ? 演算子が、机上の話ではなく実際に役立ち始める最初の場面のひとつです。ファイル操作はほとんどすべて失敗する可能性があります(ファイルが存在しないかもしれないし、権限がないかもしれません)。そのため標準ライブラリは、その失敗にきちんと向き合うことを求めてきます。
ファイル全体を読み込む
Section titled “ファイル全体を読み込む”ファイルの中身を String に読み込む、いちばんかんたんな方法です。
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("ファイルを読み込めませんでした: {e}"), }}fs::read_to_string は Result<String, std::io::Error> を返します。うまくいけば中身のテキストを持った Ok、失敗すれば理由(ファイルがない、権限がない、など)を持った Err です。
? でエラー処理をシンプルにする
Section titled “? でエラー処理をシンプルにする”ファイル操作のたびに match を書いていると、すぐに同じことの繰り返しになります。Result を返す関数の中にいるなら、(Result のページで学んだ)? 演算子を使うと、ぐっとすっきりします。
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // 失敗したらここで早期リターンする Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("エラー: {e}"), }}ファイルに書き込む
Section titled “ファイルに書き込む”use std::fs;
fn main() { let result = fs::write("output.txt", "こんにちは、ファイル!");
match result { Ok(()) => println!("書き込みに成功しました"), Err(e) => println!("書き込みに失敗しました: {e}"), }}fs::write は、ファイルがなければ新しく作り、すでにあれば中身を丸ごと上書きします。ファイルを置き換えるのではなく書き足したいときは、この点に気をつけてください。
ファイルに追記する
Section titled “ファイルに追記する”既存のファイルを上書きせずに書き足したいときは、OpenOptions を使います。
use std::fs::OpenOptions;use std::io::Write;
fn main() -> std::io::Result<()> { let mut file = OpenOptions::new() .append(true) .create(true) .open("log.txt")?;
writeln!(file, "新しいログの行")?; Ok(())}ここでは main 自体が Result を返していることに注目してください。これは覚えておく価値のある書き方です。main が Result を返すようにすると、すべてを match で包まなくても、main の中で直接 ? を使えます。
1行ずつ読み込む
Section titled “1行ずつ読み込む”大きなファイルの場合、一度に全部をメモリに読み込むのが最適とは限りません。代わりに1行ずつ読み込む方法はこうです。
use std::fs::File;use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> { let file = File::open("hello.txt")?; let reader = BufReader::new(file);
for line in reader.lines() { println!("{}", line?); }
Ok(())}どのエラーが起きたのかを調べる
Section titled “どのエラーが起きたのかを調べる”ここまでは、どのエラーも同じように扱ってきました。表示するだけです。でも実際には、何がうまくいかなかったのかによって対応を変えたいことがよくあります。ファイルがないのは、たいてい問題ありません(初めての実行なら、空の状態から始めればいいだけです)。一方、「権限がない」は、きちんと報告すべき本当の問題です。
io::Error は .kind() でエラーの種類を教えてくれます。返ってくるのは ErrorKind(英語)という列挙型なので、match で分岐できます。
use std::fs;use std::io::ErrorKind;
fn main() { // システムの一時フォルダにある、存在しないファイル let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("{}行を読み込みました", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("メモはまだありません。新しく始めます"), ErrorKind::PermissionDenied => println!("メモのファイルを読む権限がありません"), _ => println!("別の問題が起きました: {e}"), }, }}実行結果:
メモはまだありません。新しく始めますstd::env::temp_dir() はシステムの一時フォルダ(Linuxなら /tmp など)のパスを返し、.join(...) でそこにファイル名を付け足しています。ファイル操作の関数は、&str と同じように、こういうパスもそのまま受け取れます。
最後には必ず _ のアームを置いておきましょう。ErrorKind にはバリアントがたくさんあり、時間とともに新しいものが追加されていきます。_ のアームが、書かなかったものをすべて受け止めてくれます。
どちらの問題も、まず一時フォルダに自分でファイルを書き込むので、Playground も含めてどこでも動きます。
1. 保存して行を数える
Section titled “1. 保存して行を数える”fs::write で、3つのToDo項目を1行に1つずつファイルに書き込んでください。それから fs::read_to_string でファイルを読み戻し、各行の前に番号を付けて表示し、最後に合計を表示します。
ヒント:項目の間に \n を入れると、別々の行になります。text.lines() で1行ずつ取り出せます。? を使えるように、main は std::io::Result<()> を返すようにしましょう。
解答を見る
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "所有権を学ぶ\nモジュールを書く\nファイルを読む\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("全部で{count}項目"); Ok(())}実行結果:
1. 所有権を学ぶ2. モジュールを書く3. ファイルを読む全部で3項目fs::write はファイル全体を置き換えるので、何回実行しても同じ3行になります。書き込みや読み込みに失敗すると、それぞれの ? が main からエラーを返します。
2. ログに追記する
Section titled “2. ログに追記する”見出しの行を書いてログファイルを新しく始め、そこに OpenOptions と writeln! でさらに2行を追記してください。最後に、ファイル全体を読み戻して表示します。
ヒント:見出しには fs::write を使い(毎回まっさらな状態から始めるため)、追加の行には OpenOptions::new().append(true).open(&path)? を使います。ファイルはすでに改行で終わっているので、println! ではなく print! を使うと、最後に余分な空行が出ません。
解答を見る
use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== ログ ===\n")?; // まっさらな状態から始める
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "開始")?; writeln!(file, "終了")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}実行結果:
=== ログ ===開始終了use std::fs::{self, OpenOptions}; は、fs モジュールそのものと OpenOptions の両方を取り込みます。最初の fs::write がないと、実行するたびに古いファイルへ書き足され続け、ログはどこまでも大きくなります。本物のログならまさにそれが望みですが、決まった出力になるはずの練習問題では困ります。
fs::read_to_string(path)はファイル全体をStringに読み込み、Resultを返す。fs::write(path, contents)はファイルに書き込む。すでにファイルがあれば上書きする。OpenOptions::new().append(true)を使えば、置き換えずにファイルへ書き足せる。main() -> std::io::Result<()>にすると、呼び出しのたびにmatchしなくてもmainの中で直接?を使える。e.kind()でErrorKind::NotFoundのようなエラーの種類がわかるので、種類ごとに違う対応ができる。- プロジェクトのフォルダにあるファイルを使う例は手元で試すのがいちばん。
std::env::temp_dir()を使えば、どこでも動くフォルダが手に入る。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. すでに存在するパスに対して fs::write を呼ぶと、どうなりますか?
2. ファイルを上書きせずに書き足すには、どうしますか?
3. main 自体が std::io::Result<()> を返すようにすると、何ができるようになりますか?
4. fs::read_to_string(path)? が失敗したとき、? 演算子は何をしますか?
5. 大きなファイルを fs::read_to_string ではなく BufReader で1行ずつ読むのはなぜですか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”std::fs— 標準ライブラリのドキュメント(英語):create_dir_all、copy、rename、remove_fileなど、ファイルとフォルダを扱う関数がすべて載っています。- ファイルを読み込む — The Rust Programming Language 日本語版:The Bookの
minigrepプロジェクトの中でファイルを読み込みます。このページの内容とコマンドライン引数を組み合わせた例です。 - ファイル I/O — Rust By Example 日本語版:ファイルを開く、作る、1行ずつ読む、を実際に動かせる例です。
OpenOptions— 標準ライブラリのドキュメント(英語):追記、作成、切り詰めなど、ファイルを開くためのあらゆる方法です。io::ErrorKind— 標準ライブラリのドキュメント(英語):matchで分岐できるエラーの種類の完全な一覧です。