前回は、DirectoryInfo、FileInfo、Directory、File などを使ったファイルシステム操作を見ました。
今回は、ファイル入出力の中心にある ストリーム を扱います。
ストリームは、ファイル、メモリ、ネットワークなど、さまざまな入出力先を「連続したデータの流れ」として扱うための抽象化です。
ストリームとは
Stream は、バイト列を読み書きするための抽象基底クラスです。
代表的な派生型には、次のようなものがあります。
FileStream
ファイルに対してバイト単位で読み書きします。
MemoryStream
メモリ上のバイト列をストリームとして扱います。
NetworkStream
ネットワーク接続をストリームとして扱います。
Stream はバイト列の抽象化です。
テキストを扱う場合は、文字エンコーディングを考慮する必要があるため、StreamReader や StreamWriter を重ねて使うことが多いです。
FileStream でバイトを書き込む
まず、FileStream を使ってバイト列を書き込んでみます。
using System.IO;
using System.Text;
byte[] data = Encoding.UTF8.GetBytes("Hello, stream!");
using FileStream stream = new FileStream(
"sample.bin",
FileMode.Create,
FileAccess.Write);
stream.Write(data, 0, data.Length);
FileMode.Create は、ファイルを新規作成します。
既存ファイルがある場合は上書きします。
FileStream でバイトを読み込む
using System.IO;
using System.Text;
using FileStream stream = new FileStream(
"sample.bin",
FileMode.Open,
FileAccess.Read);
byte[] buffer = new byte[stream.Length];
int read = stream.Read(buffer, 0, buffer.Length);
string text = Encoding.UTF8.GetString(buffer, 0, read);
Console.WriteLine(text);
FileStream はバイト単位で扱うため、文字列へ戻すには Encoding が必要です。
using で確実に閉じる
ファイルやストリームは、使い終わったら閉じる必要があります。
C# では using を使うと、例外が発生しても確実に破棄できます。
using FileStream stream = File.OpenRead("sample.bin");
// stream を使う
ストリームを閉じ忘れると、ファイルがロックされたままになったり、書き込み内容が反映されなかったりすることがあります。
StreamWriter でテキストを書く
テキストを書く場合は、StreamWriter が便利です。
using System.IO;
using StreamWriter writer = new StreamWriter("log.txt", append: false);
writer.WriteLine("アプリケーションを開始しました。");
writer.WriteLine($"時刻: {DateTime.Now}");
追記したい場合は、append: true を指定します。
using StreamWriter writer = new StreamWriter("log.txt", append: true);
writer.WriteLine("追記しました。");
StreamReader でテキストを読む
using System.IO;
using StreamReader reader = new StreamReader("log.txt");
string allText = reader.ReadToEnd();
Console.WriteLine(allText);
1 行ずつ読むこともできます。
using StreamReader reader = new StreamReader("log.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
大きなファイルでは、ReadToEnd() で全体を一気に読むより、1 行ずつ処理する方がメモリに優しいです。
StreamWriter と StreamReader を直接作る
File.CreateText() や File.OpenText() を使うと、短く書けます。
using StreamWriter writer = File.CreateText("message.txt");
writer.WriteLine("こんにちは");
using StreamReader reader = File.OpenText("message.txt");
Console.WriteLine(reader.ReadToEnd());
内部的にはファイルストリームの上にテキスト用のリーダー/ライターを重ねている、と考えると理解しやすいです。
StringWriter と StringReader
StringWriter と StringReader は、文字列を入出力先として扱います。
using System.IO;
using StringWriter writer = new StringWriter();
writer.WriteLine("1 行目");
writer.WriteLine("2 行目");
string result = writer.ToString();
Console.WriteLine(result);
文字列を読み取り元として扱う例です。
using System.IO;
string source = """
apple
banana
orange
""";
using StringReader reader = new StringReader(source);
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line.ToUpperInvariant());
}
StringWriter / StringReader は、ファイルではなく文字列を対象にしたいテストや変換処理で便利です。
BinaryWriter でバイナリデータを書く
BinaryWriter を使うと、数値や文字列をバイナリ形式で書き込めます。
using System.IO;
using FileStream stream = File.Create("person.dat");
using BinaryWriter writer = new BinaryWriter(stream);
writer.Write(1);
writer.Write("Alice");
writer.Write(29);
writer.Write(true);
このファイルはテキストファイルではないため、メモ帳などで見ても読める形式ではありません。
プログラム同士で効率よくやり取りするための形式です。
BinaryReader でバイナリデータを読む
書き込んだ順番と同じ順番で読み取ります。
using System.IO;
using FileStream stream = File.OpenRead("person.dat");
using BinaryReader reader = new BinaryReader(stream);
int id = reader.ReadInt32();
string name = reader.ReadString();
int age = reader.ReadInt32();
bool active = reader.ReadBoolean();
Console.WriteLine($"{id}: {name}, {age}, {active}");
バイナリ読み書きでは、書き込み順序と読み取り順序が契約になります。
途中で項目を増やしたり順番を変えたりすると、古いデータを読めなくなる可能性があります。
File クラスの便利メソッドとの違い
短い処理なら、File.WriteAllText() や File.ReadAllText() が便利です。
File.WriteAllText("note.txt", "hello");
string text = File.ReadAllText("note.txt");
一方、ストリームやリーダー/ライターは、次のような場面で向いています。
- 大きなファイルを少しずつ読みたい
- 追記したい
- バイナリ形式で扱いたい
- ネットワークやメモリなど、ファイル以外の入出力先も同じ考え方で扱いたい
- エンコーディングやバッファリングを細かく制御したい
小さなファイルでは便利メソッド、大きな処理や細かい制御ではストリーム、と考えると選びやすくなります。
非同期読み書き
ファイル入出力は時間がかかることがあります。
UI アプリやサーバーアプリでは、非同期 API を使うとスレッドをブロックしにくくなります。
await File.WriteAllTextAsync("async.txt", "非同期で書き込みます。");
string text = await File.ReadAllTextAsync("async.txt");
Console.WriteLine(text);
StreamReader / StreamWriter にも非同期メソッドがあります。
using StreamWriter writer = new StreamWriter("async-log.txt");
await writer.WriteLineAsync("非同期ログ");
大量データや高頻度アクセスでは、同期/非同期の選択が応答性に影響します。
まとめ
ストリームは、バイト列の流れを扱うための抽象化です。
ファイルだけでなく、メモリやネットワークにも同じ考え方を適用できます。
テキストには StreamReader / StreamWriter、文字列には StringReader / StringWriter、バイナリには BinaryReader / BinaryWriter が便利です。
次回は、ファイル変更を監視する方法と、オブジェクトを保存可能な形に変換する考え方へ進みます。