Menu

C#のファイルの書き込みと読み込み:File、StreamWriter、StreamReader

C#でファイルに書き込み、ファイルを読み込む方法を解説します。ファイル全体を扱うFile.WriteAllText、ReadAllText、AppendAllText、WriteAllLines、ReadAllLines、大きなファイルのためのStreamWriterとStreamReader、Path.Combineによるパス、ディレクトリ、エンコーディング、そしてファイルを扱うコードが処理すべき例外を扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

System.IO 名前空間には、ファイルを扱うためのすべてがあります。ほとんどの作業は、静的な File クラスの呼び出し1回で足ります。ファイルを開き、読み書きし、閉じてくれます。大きなファイルや細かな制御には StreamReader と StreamWriter を使います。以下のすべての例には using System.IO; を加えます。

ファイル全体の書き込みと読み込み

File.WriteAllText は、渡したテキストでファイルを作成します(既存のものは上書きします)。File.ReadAllText はファイル全体を1つの文字列として返します。File.AppendAllText は末尾に追加します。

出力:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

"notes.txt" のような相対パスは、プロセスのカレントディレクトリを基準に解決されます。これは必ずしもプログラムがあるフォルダーではありません(サービス、スケジュールされたタスク、テストランナーは別の場所で起動することがあります)。アプリケーションと一緒に配布するファイルには、AppContext.BaseDirectory からパスを組み立てます。

WriteAllText は確認せずに上書きします。古い内容を失うことが問題なら、先に File.Exists を確認するか、一時ファイルに書き込んでから File.Move で所定の場所に移します。

行:WriteAllLines、ReadAllLines、ReadLines

1行に1レコードのファイルはよくあるので、専用のメソッドがあります。WriteAllLines は各文字列の後に改行を書き、ReadAllLines は string[] を返します。

出力:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

2つの読み込み方の違いは、サイズにとって重要です。ReadAllLines は戻る前にファイル全体をメモリに読み込みます。ReadLines は反復に合わせて読む遅延評価の IEnumerable<string> を返すので、5 GBのログを foreach しても数キロバイトしか使いません。LINQ と組み合わせると、ReadLines(...).Where(...).Take(10) は一致が10個得られた時点で読むのをやめます。

Split(',') によるCSVの解析は、自分で作った単純なファイルなら動きます。現実のCSVにはカンマを含む引用符付きのフィールドがあるので、それにはCsvHelperのようなライブラリを使います。

decimal.Parse の InvariantCulture に注目してください。これがないと、ドイツ語やポルトガル語に設定されたマシンでは、"4.50" の解析でドットが桁区切りとして読まれます。

StreamWriterとStreamReader

File のメソッドは、呼び出しのたびにファイルを開いて閉じます。時間をかけて多くの部分を書き込む場合や、LINQを使わずにメモリに収まらないほど大きなファイルを読む場合は、ストリームを一度だけ開きます。例外が投げられても閉じられるように、必ず using で囲みます。閉じられていないライターはバッファーを書き出さないことがあり、ファイルが空になったり途中で切れたりします。

出力:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine はファイルの終わりで null を返し、それが while ループを終わらせます。StreamWriter は出力をバッファーし、バッファーがいっぱいになったとき、Flush() を呼んだとき、破棄されたときにディスクへ書き込みます。using ブロックは最後のものを保証します。

C# 8以降は、using 宣言を使えば、余分な波かっこなしで、外側のブロックの終わりでストリームが破棄されます。

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

パスとディレクトリ

パスは文字列の連結ではなく Path.Combine で組み立てます。オペレーティングシステムに合った区切り文字(Windowsでは \、LinuxとmacOSでは /)を挿入し、重複もさせません。Path クラスはパスを分解することもでき、Directory はフォルダーを作成して一覧を取得します。

出力:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

1つ落とし穴があります。Path.Combine の後ろの引数がルートから始まっている(/ や \、Windowsではドライブ文字で始まる)と、その前のものはすべて捨てられるので、Path.Combine("reports", "/2026") は /2026 になります。相対的な部分を渡すか、何も捨てない Path.Join(.NET Core 3.0以降)を使います。

Directory.GetFiles はファイルシステムが返す順序でファイルを返し、その順序はWindowsとLinuxで異なるので、順序が重要なら結果を並べ替えます。Directory.EnumerateFiles は遅延評価版で、ReadAllLines に対する ReadLines と同じ関係です。サブフォルダーも含めるには SearchOption.AllDirectories を渡します。

エラーの処理

ファイル操作は、プログラムの外の理由で失敗します。ファイルがない、フォルダーが存在しない、別のプロセスが開いている、ディスクがいっぱい、権限が正しくない、などです。それぞれに独自の例外型があります。FileNotFoundException と DirectoryNotFoundException は IOException から派生しているので、それより前にキャッチします。UnauthorizedAccessException は派生していないので、独自の catch が必要です。

出力:

missing file, using defaults
missing folder, using defaults
theme=dark

読み込みの前の File.Exists のほうが簡単に見えますが、try の必要性はなくなりません。確認と読み込みの間の瞬間に、ファイルが消えたり、別のプログラムにロックされたりすることがあります。Exists は何をするかを決めるために使い、実際に起きたことを乗り切るには catch を使います。

エンコーディング

File.WriteAllText、WriteAllLines、StreamWriter は、既定でBOMなしのUTF-8で書き込み、読み込む側は、BOMがあればそれからUTF-8、UTF-16、UTF-32を判別します。ファイルが特定の形式でなければならない場合は Encoding を渡します。

出力:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é と ã はUTF-8でそれぞれ2バイトを使うので、14文字に16バイトが必要で、読み込む側がBOMを認識するので3つのファイルはすべて正しく読み戻されます。Encoding.UTF8 は既定と同じに見えますが、BOMを追加します。一部のツール(古いCSVのインポーター、シェルスクリプト)は、それを最初の行の先頭のゴミとして表示します。BOMなしの明示的なUTF-8には new UTF8Encoding(false) を使います。間違ったエンコーディングでファイルを読んでも例外は投げられず、置換文字が生じるだけなので、自分で作っていないファイルのエンコーディングは確認しておきます。

非同期のファイルI/O

WebアプリやUIのコードでは、ディスクのI/Oでスレッドをブロックするのは無駄です。.NET Core 2.0以降には、File のメソッドの非同期版があります。

await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");

StreamReader.ReadLineAsync と StreamWriter.WriteLineAsync も、ストリームに対して同じように動きます。

よくある間違い

  • ストリームを破棄しない。 using がないと、バッファーされたテキストがディスクに届かないことがあり、ガベージコレクターが動くまでファイルはロックされたままになります。
  • + "\\" + でパスを組み立てる。 LinuxとmacOSで壊れます。Path.Combine を使います。
  • 巨大なファイルを ReadAllText や ReadAllLines で読み込む。 ReadLines か StreamReader でストリームとして読みます。
  • カレントディレクトリに頼る。 プログラムの起動方法によって変わります。絶対パスか AppContext.BaseDirectory を使います。
  • ファイルの数値をマシンのカルチャで解析する。 データファイルには CultureInfo.InvariantCulture を渡します。
  • File.Exists で例外が防げると思う。 確認と使用の間にファイルが変わることがあります。

よくある質問

C#でファイルにテキストを書き込むには?

File.WriteAllText("notes.txt", text); は、ファイルを作成し(存在すれば上書きし)、文字列をUTF-8で書き込み、閉じます。行のコレクションには File.WriteAllLines(path, lines) を、置き換えずに末尾に追加するには File.AppendAllText(path, text) を使います。3つとも System.IO にあります。

C#でテキストファイルを読み込むには?

string text = File.ReadAllText("notes.txt"); はファイル全体を1つの文字列に読み込み、string[] lines = File.ReadAllLines(path); は行に分割します。大きなファイルには、すべてを読み込む代わりに1行ずつ読む File.ReadLines(path) を foreach で使うか、StreamReader を使います。

C#でファイルに追記するには?

File.AppendAllText(path, text) はファイルの末尾にテキストを追加し、ファイルがなければ作成します。File.AppendAllLines(path, lines) は行について同じことをします。ストリームでは追記モードで開きます:new StreamWriter(path, append: true)。テキストを追記するときは、Environment.NewLine や \n を自分で加えることを忘れないでください。

C#でファイルを1行ずつ読み込むには?

foreach (string line in File.ReadLines(path)) { ... } はファイルを1行ずつストリームで読むので、ギガバイト単位のログでもメモリの使用量は一定のままです。古い同等の方法は、using ブロックの中の StreamReader で while ((line = reader.ReadLine()) != null) とすることです。大きなファイルには ReadAllLines は避けます。先にすべての行を配列に読み込むからです。

C#でファイルが存在するか確認するには?

File.Exists(path) は、ファイルが存在し、プログラムから見える場合に true を返し、Directory.Exists(path) はフォルダーについて同じことをします。例外を避けるためにこれに頼ってはいけません。確認と読み込みの間にファイルが削除されたりロックされたりすることがあるので、実際のファイル操作の周りでは FileNotFoundException と IOException を処理します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める