1. クラス FileStream: チャンクでの操作
FileStreamは、ディスク上のファイルに直結した水道管みたいなものだと思えばOK。このパイプを通して、データをコントロールしながら流せる:ファイルにバイトを書き込んだり(書き込み)、ファイルからバイトを受け取ったり(読み込み)できる。もっと高レベルなメソッドは「コップ一杯の水」をくれるだけだけど、FileStreamは「蛇口」を直接操作できて、水(バイト)の流れを好きなように調整できるんだ。
FileStreamはバイトレベルで動く。つまり、テキストだろうが画像だろうが、全部ただのバイト列として扱う。テキストファイルをFileStreamで扱う場合は、自分で文字列をバイトに変換(例えばエンコーディング、UTF-8とか)して書き込んだり、逆に読み込んだバイトを文字列に戻したりする必要があるよ。
どんな時にFileStreamが必要?
- 超でかいファイルの操作: ファイルがデカすぎて、全部メモリに読み込むのが無理or非効率な時(例:ギガバイト級のログや動画ファイル)。FileStreamなら、データをチャンク(部分)ごとに読み書きできて、メモリ効率がいい。
- バイナリデータ: 普通のテキストじゃないファイル(画像、音声、動画、シリアライズされたオブジェクト、DBファイルなど)を扱うなら、FileStreamが基本ツール。バイトに直接アクセスできるからね。
- アクセスモードの細かい制御: ファイルを読み込み*と*書き込み両方で開きたい?他のプログラムからアクセスできないようにしたい?逆に、複数プロセスで同時に読みたい?FileStreamなら全部コントロールできる。
- 非同期操作: 最近の高パフォーマンスなアプリ(例:Webサーバー)では、ファイル操作でメインスレッドを止めたくない。FileStreamは非同期メソッド(ReadAsyncやWriteAsync)に対応してて、アプリのレスポンスを保てる。
- 部分的な読み書きやランダムアクセス: ファイルの特定の場所(例:500バイト目)からデータを読みたい、または途中に書き込みたい時、FileStreamならファイルポインタの位置を自由に動かせる。
初心者あるあるミス: 小さいファイルに1行だけテキストを書き込むみたいな簡単な用途でFileStreamを使っちゃうこと。こういう時はオーバースペック。FileStreamはもっと特殊で複雑なシナリオ向けのツールだよ。
2. FileStreamの作成とオープン
ファイルをFileStreamで操作するには、まずインスタンスを作る必要がある。これはコンストラクタで作るんだけど、いくつか重要なパラメータがあって、それでファイルとのやり取り方法を決めるんだ。
using System;
using System.IO; // FileStreamを使うには必須
using System.Text; // エンコーディング(文字列⇔バイト変換)用
FileStreamでテキストファイルを基本的に読む例:
まず、読むためのファイルを作っておこう。
// デモ用にシンプルなテキストファイルを作成
string path = "example_filestream.txt";
// 読み込み用にストリームを開く
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
// 読み込んだデータを一時保存するバッファ(バイト配列)を作成
byte[] buffer = new byte[fs.Length];
// ストリームからバッファにバイトを読み込む
int bytesRead = fs.Read(buffer, 0, buffer.Length);
// 読み込んだバイトを、指定したエンコーディング(例:UTF-8)で文字列に変換
string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("FileStreamでファイルから読み込んだ: " + content);
fs.Close(); //ストリームを閉じる
主なパラメータ
- FileMode: ファイルの開き方(
Open,Create,Append,OpenOrCreateなど) - FileAccess: ファイルに対して何をするか(
Read,Write,ReadWrite) - FileShare: 他のプロセスが同時にこのファイルを使えるか(普通はシンプルな用途なら不要)
よくあるミス: FileStreamを閉じ忘れると、ファイルが「ロック」されて、後で削除や再オープンできなくなることがあるよ!
3. FileStreamのコンストラクタのパラメータ
FileStreamのコンストラクタは、ファイルをどう開くかを細かく設定できる。主なパラメータはこれ:
new FileStream(
string path, // ファイルのパス(絶対or相対)
FileMode mode, // ファイルの開き方(作成、オープン、上書きなど)
FileAccess access, // 許可するアクセス(読み込みのみ、書き込みのみ、読み書き両方)
FileShare share // ファイルが開いてる間、他プロセスがどうアクセスできるか(オプション)
);
FileModeとFileAccessの列挙値を見てみよう:
FileMode(ファイルの開き方):
ファイルを開く時、OSがどう扱うかを決める。
FileMode.Open: 既存のファイルを開く。 指定パスにファイルがなければFileNotFoundExceptionが出る。FileMode.Create: 新しいファイルを作成。 同じ名前のファイルがあれば完全に上書き(中身は消える)。
FileAccess(ファイルのアクセス権):
開いたファイルに対して、どんな操作が許可されるか。
FileAccess.Read: ファイルは読み込み専用。書き込みはできない。FileAccess.Write: ファイルは書き込み専用。読み込みはできない。FileAccess.ReadWrite: ファイルは読み書き両方OK。一番柔軟だけど、ストリーム位置の管理がちょっと難しい。
FileShare(共有アクセス):
自分のプログラムがファイルを開いてる間、他のプロセスがどうやってそのファイルを開けるかを決める。ロック回避に重要。
FileShare.Read: 他プロセスは読み込みだけOK、書き込みはNG。FileShare.Write: 他プロセスは書き込みだけOK、読み込みはNG。FileShare.ReadWrite: 他プロセスも読み書き両方OK。一番ゆるいけど、複数プログラムが同時にファイルをいじると競合が起きやすい。
モードの詳細は次のレクチャーで!
4. FileStreamでのデータ読み書き
FileStreamを使う時は、バイト単位で操作する。つまり、テキストデータの場合は、文字列とバイト配列の変換をエンコーディングクラス(例:System.Text.Encoding.UTF8)でやる必要があるよ。
FileStreamでファイルにデータを書き込む
書き込み時は、データ(例:文字列)をバイト配列に変換してから、そのバイトをストリームに書き込む。
string outputPath = "user_data.txt";
string userName = "イワン・ペトロフ";
// 文字列をUTF-8エンコーディングでバイト配列に変換
byte[] userNameBytes = Encoding.UTF8.GetBytes(userName);
// 書き込み用にFileStreamを開く
FileStream fsWrite = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
// バイト配列をストリームに書き込む
fsWrite.Write(userNameBytes, 0, userNameBytes.Length);
Console.WriteLine($"名前 '{userName}' をファイル '{outputPath}' に正常に書き込みました。");
fsWrite.Close(); //ストリームを閉じる
FileStreamでファイルからデータを読む
読み込み時は、ストリームからバイトをバッファに読み込んで、それを必要な形式(例:文字列)に変換する。
string inputPath = "user_data.txt";
// 読み込み用にFileStreamを開く
FileStream fsRead = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
// ファイルサイズ分のバッファを作成
byte[] buffer = new byte[fsRead.Length];
// ストリームからバッファにバイトを読み込む
int bytesRead = fsRead.Read(buffer, 0, buffer.Length);
// 読み込んだバイトを文字列に変換
string loadedUserName = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"ファイルからの名前(FileStream経由): {loadedUserName}");
fsRead.Close(); //ストリームを閉じる
5. 大きなファイルの操作
FileStreamの最大のメリットは、File.ReadAll...系メソッドと違って、部分的にファイルを読み書きできること。これは、メモリに全部入りきらない大きなファイルを扱う時に超重要!
大きなファイルをチャンクで読む
ファイルを部分的に読む時は、固定サイズのバッファ(バイト配列)を使って、ファイルの終わりまで何度もデータを読み込む。
string bigFilePath = "bigfile.bin"; //大きなファイル
int bufferSize = 4096; // バッファサイズ(例:4KB)
byte[] buffer = new byte[bufferSize]; // バッファ作成
int bytesRead; // 実際に読み込んだバイト数
long totalBytesRead = 0; // 合計読み込みバイト数
// ストリームを開く
FileStream fs = new FileStream(bigFilePath, FileMode.Open, FileAccess.Read);
int chunkNumber = 1;
// fs.Read()が0より大きい間ループ
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
Console.WriteLine($"チャンク{chunkNumber++}: {bytesRead}バイト読み込み。合計: {totalBytesRead}バイト。");
// !!! ここで読み込んだ「チャンク」データを処理する !!!
}
fs.Close(); //ストリームを閉じる
重要な注意: fs.Read(buffer, offset, count)は、bufferを全部埋めるとは限らない。実際に読み込んだバイト数を返すので、特にファイルの終わり付近ではcountより少なくなることも。必ずbytesReadの値を使ってデータを正しく処理しよう。
大きなデータをチャンクで書き込む
読み込みと同じく、大量のデータをファイルに部分的に書き込むこともできる。
string bigOutputPath = "big_output.txt";
string longText = new string('A', 1_000_000); // 'A'が100万個の文字列
byte[] longTextBytes = Encoding.UTF8.GetBytes(longText);
int writeBufferSize = 1024; // 1KBずつ書き込む
// FileMode.Create: ファイルを作成
FileStream fsWrite = new FileStream(bigOutputPath, FileMode.Create, FileAccess.Write);
for (int i = 0; i < longTextBytes.Length; i += writeBufferSize)
{
// 今回書き込むバイト数を計算
int bytesToWrite = Math.Min(writeBufferSize, longTextBytes.Length - i);
// longTextBytesのi番目からbytesToWrite分書き込む
fsWrite.Write(longTextBytes, i, bytesToWrite);
Console.WriteLine($"{bytesToWrite}バイト書き込み");
}
fsWrite.Close(); //ストリームを閉じる
6. 意外な落とし穴とよくあるミス
FileStreamみたいな強力なツールでも、気をつけるべきポイントがあるよ:
バッファリングとFlush(): さっきも言ったけど、データはメモリ上のバッファに残ってて、まだディスクに書き込まれてないことがある。usingを使わずにClose()やDispose()を呼ばずにプログラムを終わらせると、データが消えることも。必ずFlush()を呼ぶか、using/Close()に頼ろう。
例外処理: 入出力エラー(例:IOExceptionはディスク容量不足、UnauthorizedAccessExceptionは権限不足、FileNotFoundExceptionはOpenモードで存在しないファイルを開こうとした時など)はよくある。FileStreamの操作は必ずtry-catchで囲もう。
エンコーディング: FileStream(バイト操作)でテキストデータを扱う時は、正しいエンコーディング選びは全部自分の責任(Encoding.UTF8, Encoding.ASCII, Encoding.Unicodeなど)。間違ったエンコーディングだと文字化けするよ。
位置管理: Seek()を使う時は、offsetやoriginの値に注意。ファイルの外や予想外の場所に移動しないように。
ファイルロック(FileShare): FileShare.Noneでファイルを開くと、他プロセスはアクセスできなくなる。他のプログラム(や自分のプログラムの別スレッド)が同じファイルを使おうとすると問題になる。FileShareのモードは用途に合わせて選ぼう。
GO TO FULL VERSION