CodeGym /コース /C# SELF /クラス FileStream

クラス FileStream

C# SELF
レベル 35 , レッスン 3
使用可能

1. クラス FileStream: チャンクでの操作

FileStreamは、ディスク上のファイルに直結した水道管みたいなものだと思えばOK。このパイプを通して、データをコントロールしながら流せる:ファイルにバイトを書き込んだり(書き込み)、ファイルからバイトを受け取ったり(読み込み)できる。もっと高レベルなメソッドは「コップ一杯の水」をくれるだけだけど、FileStreamは「蛇口」を直接操作できて、水(バイト)の流れを好きなように調整できるんだ。

FileStreamバイトレベルで動く。つまり、テキストだろうが画像だろうが、全部ただのバイト列として扱う。テキストファイルをFileStreamで扱う場合は、自分で文字列をバイトに変換(例えばエンコーディングUTF-8とか)して書き込んだり、逆に読み込んだバイトを文字列に戻したりする必要があるよ。

どんな時にFileStreamが必要?

  • 超でかいファイルの操作: ファイルがデカすぎて、全部メモリに読み込むのが無理or非効率な時(例:ギガバイト級のログや動画ファイル)。FileStreamなら、データをチャンク(部分)ごとに読み書きできて、メモリ効率がいい。
  • バイナリデータ: 普通のテキストじゃないファイル(画像、音声、動画、シリアライズされたオブジェクト、DBファイルなど)を扱うなら、FileStreamが基本ツール。バイトに直接アクセスできるからね。
  • アクセスモードの細かい制御: ファイルを読み込み**書き込み両方で開きたい?他のプログラムからアクセスできないようにしたい?逆に、複数プロセスで同時に読みたい?FileStreamなら全部コントロールできる。
  • 非同期操作: 最近の高パフォーマンスなアプリ(例:Webサーバー)では、ファイル操作でメインスレッドを止めたくない。FileStreamは非同期メソッド(ReadAsyncWriteAsync)に対応してて、アプリのレスポンスを保てる。
  • 部分的な読み書きやランダムアクセス: ファイルの特定の場所(例: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        // ファイルが開いてる間、他プロセスがどうアクセスできるか(オプション)
);

FileModeFileAccessの列挙値を見てみよう:

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は権限不足、FileNotFoundExceptionOpenモードで存在しないファイルを開こうとした時など)はよくある。FileStreamの操作は必ずtry-catchで囲もう。

エンコーディング: FileStream(バイト操作)でテキストデータを扱う時は、正しいエンコーディング選びは全部自分の責任Encoding.UTF8, Encoding.ASCII, Encoding.Unicodeなど)。間違ったエンコーディングだと文字化けするよ。

位置管理: Seek()を使う時は、offsetoriginの値に注意。ファイルの外や予想外の場所に移動しないように。

ファイルロック(FileShare): FileShare.Noneでファイルを開くと、他プロセスはアクセスできなくなる。他のプログラム(や自分のプログラムの別スレッド)が同じファイルを使おうとすると問題になる。FileShareのモードは用途に合わせて選ぼう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION