画像ファイルや実行ファイル(DLL/EXE)などのバイナリデータを扱う際、File.ReadAllBytes で一括でメモリに読み込むと、巨大なファイルを扱った際にメモリ不足(OutOfMemory)になるリスクがあります。
FileStream と yield return を組み合わせて、決まったサイズ(バッファサイズ)ごとに分割して読み込むことで、メモリ消費を抑えながら安全に処理する方法を解説します。
実装サンプル:バイナリデータの分割読み込み
以下のコードでは、ファイルを 1KB (1024バイト) ずつ読み込み、その都度コンソールにサイズを出力しています。
サンプルコード
C#
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public class Program
{
public static void Main()
{
// 読み込み対象のファイルパス
string filePath = "test_data.bin";
// 1. テスト用に適当なバイナリファイルを作成 (2500バイト)
if (!File.Exists(filePath))
{
byte[] dummyData = new byte[2500];
new Random().NextBytes(dummyData); // ランダムな値で埋める
File.WriteAllBytes(filePath, dummyData);
}
Console.WriteLine($"ファイルサイズ: {new FileInfo(filePath).Length} バイト\n");
// 2. FileStreamを開く
using (FileStream fs = File.OpenRead(filePath))
{
// 3. 自作メソッドを使って 1024バイトずつ取り出す
int count = 1;
foreach (byte[] chunk in ReadBinaryFile(fs, 1024))
{
Console.WriteLine($"[{count}回目] 読み込みデータ長: {chunk.Length} bytes");
// ここで chunk に対して必要なバイナリ解析処理を行う
count++;
}
}
}
// ストリームから指定サイズずつ読み出して返すメソッド (イテレータ)
static IEnumerable<byte[]> ReadBinaryFile(Stream stream, int bufferSize)
{
// 読み込み用バッファ
byte[] buffer = new byte[bufferSize];
int readSize;
// stream.Read は実際に読めたバイト数を返す (0なら終了)
while ((readSize = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// バッファがいっぱいまで読めた場合と、最後の方で端数しか読めない場合がある
if (readSize == bufferSize)
{
// バッファをそのまま返す(コピーして返すと安全だが、今回は簡略化のためコピー推奨)
// yield return buffer; // 参照渡しになるため注意が必要
yield return buffer.ToArray();
}
else
{
// 実際の読み込みサイズに合わせて切り出して返す
yield return buffer.Take(readSize).ToArray();
}
}
}
}
実行結果
ファイルサイズ: 2500 バイト
[1回目] 読み込みデータ長: 1024 bytes
[2回目] 読み込みデータ長: 1024 bytes
[3回目] 読み込みデータ長: 452 bytes
解説と技術的なポイント
1. FileStream.Read
バイナリ読み込みの基本メソッドです。 stream.Read(buffer, offset, count) の形式で呼び出します。 戻り値として「実際に読み込めたバイト数」が返ってきます。これが 0 の場合、ファイルの終端(EOF)に達したことを意味します。
2. yield return の活用
巨大なファイルを扱う場合、すべての分割データを List<byte[]> に入れてから返すと、結局メモリを大量消費してしまいます。 yield return を使うことで、呼び出し元(Mainメソッドのforeach)がデータを必要としたタイミングで、1ブロックずつ読み込みと処理が行われるため、メモリ効率が非常に良くなります。
3. Take(readSize).ToArray()
読み込みループの最後(ファイルの末尾)では、バッファサイズ(今回は1024)いっぱいまでデータがないことがあります(実行結果の452バイトの部分)。 そのため、buffer をそのまま返すのではなく、有効なデータが入っている部分だけを切り出して返す処理が必要です。
