【C#】EndOfStreamExceptionが発生する原因と例外を防ぐ具体的なコード例
EndOfStreamException
はSystem.IO
の例外で、ストリーム末尾を超えて読み込もうとした瞬間に発生します。
原因はデシリアライズ前にPosition
を戻し忘れる、ループ中にEndOfStream
やLength
を確認しないなどです。
対策は末尾チェック、Seek
で先頭に戻す、catchでログして安全に終了することです。
EndOfStreamExceptionを引き起こす基礎構造
ストリームAPIの仕組み
C#のストリームAPIは、ファイルやメモリ、ネットワークなどのデータソースから連続したバイト列を読み書きするための仕組みです。
ストリームは基本的に「読み取り位置(Position)」を持ち、そこからデータを読み込んだり書き込んだりします。
ストリームの終端に達すると、それ以上の読み取りはできません。
この仕組みを理解することが、EndOfStreamException
の発生原因を把握する第一歩です。
基本プロパティ(PositionとLength)
ストリームの重要なプロパティとして、Position
とLength
があります。
Position
は現在の読み書き位置を示すlong
型の値です。ストリームの先頭は0
で、読み込みや書き込みを行うたびにこの値は進みますLength
はストリーム全体の長さ(バイト数)を示します。読み取り可能なデータの総量を表すため、Position
がLength
に達するとストリームの終端に到達したことになります
例えば、ファイルストリームの場合、Length
はファイルのサイズに相当します。
Position
がLength
を超えることは通常ありませんが、プログラムの誤りでPosition
を不適切に操作すると、読み取り時に例外が発生する原因になります。
Read系メソッドの挙動
ストリームからデータを読み取るメソッドには、Read
、ReadByte
、ReadLine
(StreamReader
の場合)などがあります。
これらのメソッドは、現在のPosition
から指定されたバイト数や文字数を読み込み、Position
を進めます。
Read
メソッドは、指定したバッファに読み込めるだけのデータを格納し、実際に読み込んだバイト数を返します。読み込めるデータがない場合は0
を返しますReadByte
は1バイトだけ読み込み、読み込めなければ-1
を返しますStreamReader
のReadLine
は、改行までの文字列を読み込みます。終端に達するとnull
を返します
これらのメソッドは、ストリームの終端に達した場合に読み込みを停止する設計ですが、BinaryReader
のようにバイナリデータを読み込む際に、期待したサイズのデータが存在しないとEndOfStreamException
が発生します。
これは、読み込み要求がストリームの終端を超えているためです。
.NETランタイムが例外を投げる条件
EndOfStreamException
は、System.IO
名前空間に属し、IOException
を継承した例外クラスです。
この例外は、ストリームの終端を超えて読み込みを試みた場合にスローされます。
具体的には以下のような条件で発生します。
- 読み込み要求がストリームの残りデータ量を超えた場合
例えば、BinaryReader.ReadInt32()
は4バイトの読み込みを要求しますが、ストリームの残りが3バイト以下の場合、EndOfStreamException
が発生します。
- シリアライズやデシリアライズ処理でストリームの位置が不適切な場合
デシリアライズ時にストリームのPosition
が終端に近い、または終端を超えていると、読み込みができず例外が発生します。
- ストリームの終端を正しく検出せずに読み込みを続けた場合
例えば、StreamReader
のEndOfStream
プロパティを使わずにReadLine
を繰り返すと、終端を超えて読み込もうとして例外が起きることがあります。
- 非同期読み込みでキャンセルや切断が発生した場合
ネットワークストリームなどで接続が切れた際に、読み込みが途中で終わると例外が発生することがあります。
このように、EndOfStreamException
はストリームの終端を超えた読み込み操作が原因で発生します。
したがって、ストリームのPosition
やLength
を適切に管理し、終端を検出してから読み込みを行うことが重要です。
これにより、例外の発生を未然に防ぐことができます。
主な発生パターン
ファイルI/Oでのケース
固定長データの読み取りミス
ファイルから固定長のバイナリデータを読み取る際に、読み込みサイズの誤りやファイルの破損により、期待したバイト数が取得できない場合があります。
たとえば、4バイトの整数を読み込むつもりが、ファイルの終端に近くて3バイトしか残っていないと、BinaryReader.ReadInt32()
はEndOfStreamException
をスローします。
この問題は、ファイルのサイズや残りバイト数を事前にチェックせずに読み込みを行うことが原因です。
ファイルの破損や不完全な書き込みがある場合も同様に発生します。
連続ReadLineでEOFを無視
StreamReader
のReadLine
メソッドを使ってテキストファイルを読み込む際、EndOfStream
プロパティを確認せずに無限ループや条件判定を行うと、ファイルの終端を超えて読み込みを試みてEndOfStreamException
が発生することがあります。
たとえば、以下のようにwhile(true)
でReadLine
を繰り返し、null
チェックを怠るケースです。
StreamReader reader = new StreamReader("sample.txt");
while (true)
{
string line = reader.ReadLine();
// lineがnullの可能性を無視して処理を続ける
}
このようなコードは、ファイル終端に達したときに例外を引き起こします。
EndOfStream
やReadLine
の戻り値null
を適切にチェックすることが重要です。
ネットワークストリームでのケース
TCP接続切断時の読み取り
TCPソケットのネットワークストリームでデータを読み込む際、接続が相手側で切断されると、読み込み操作が途中で終了し、EndOfStreamException
が発生することがあります。
特に、NetworkStream
やBinaryReader
を使って固定長のデータを読み込む場合、切断により期待したバイト数が得られず、例外がスローされます。
接続の状態を監視し、切断時には読み込みを中断する処理が必要です。
WebSocketメッセージ処理
WebSocket通信でメッセージを受信する際、メッセージの終端を正しく検出しないと、ストリームの終端を超えて読み込みを試みてEndOfStreamException
が発生します。
WebSocketはフレーム単位でデータを送受信しますが、フレームの終わりを判定せずに連続して読み込むと、バッファの終端を超える読み込みが起きやすいです。
フレームの長さやメッセージの終端を正確に把握し、読み込み範囲を制御することが重要です。
圧縮ストリーム使用時
GZipStreamのサイズ超過
GZipStream
などの圧縮ストリームを使って圧縮データを展開する際、圧縮データの終端を超えて読み込みを行うとEndOfStreamException
が発生します。
圧縮ストリームは元のデータサイズと圧縮後のサイズが異なるため、展開時に読み込みサイズを誤ると、ストリームの終端を超えて読み込もうとすることがあります。
圧縮データの正確なサイズ管理や、読み込みループでの終端チェックが必要です。
シリアライズ・デシリアライズ
BinaryFormatterのバージョン不整合
BinaryFormatter
を使ったシリアライズ・デシリアライズ処理で、シリアライズ時とデシリアライズ時のクラス定義やバージョンが異なる場合、ストリームの読み込み位置がずれてEndOfStreamException
が発生することがあります。
たとえば、シリアライズしたオブジェクトのフィールドが増減したり、型が変更された場合、デシリアライズ時に期待するデータ構造と実際のストリーム内容が合わず、読み込みが終端を超えてしまいます。
この問題を防ぐには、バージョン管理を徹底し、互換性のあるシリアライズ設計を行うことが重要です。
メモリストリーム使用時
書き込み忘れによるサイズ不足
MemoryStream
にデータを書き込んだ後、書き込みが完了していなかったり、Flush
やPosition
のリセットを忘れると、読み込み時に期待したデータが存在せずEndOfStreamException
が発生します。
たとえば、書き込み後にPosition
を先頭に戻さずに読み込みを開始すると、ストリームの終端に位置しているため、読み込みができません。
また、書き込み途中でストリームを読み込もうとすると、データが不完全で例外が起きることがあります。
書き込み完了後は必ずPosition
を0
に戻し、読み込み前にデータの整合性を確認してください。
サードパーティライブラリ内での例
サードパーティ製のライブラリやフレームワークでストリームを扱う際、内部処理の不具合や使い方の誤りによりEndOfStreamException
が発生することがあります。
たとえば、独自のシリアライズ形式や圧縮形式を扱うライブラリで、ストリームの終端チェックが不十分な場合や、APIの仕様に合わない読み込みを行うと例外が起きます。
このような場合は、ライブラリのドキュメントをよく確認し、正しい使い方を守ることが重要です。
また、例外発生時のスタックトレースを解析し、どの処理で終端超過が起きているかを特定することがトラブルシューティングの鍵となります。
原因特定のポイント
コールスタック解析
EndOfStreamException
が発生した際、まずは例外のコールスタックを詳細に確認することが重要です。
コールスタックは、例外がスローされたメソッドの呼び出し履歴を示しており、どの処理でストリームの終端を超えた読み込みが行われたかを特定できます。
Visual StudioなどのIDEでは、例外発生時にコールスタックウィンドウでスタックトレースを確認できます。
特に、BinaryReader
やStreamReader
の読み込みメソッドがどの行で呼ばれているかを注視してください。
自作のメソッドやライブラリの呼び出し階層も含めて解析することで、問題の根本箇所を絞り込めます。
また、非同期処理やイベント駆動のコードでは、コールスタックが複雑になることがあるため、例外発生時のスレッドコンテキストも確認するとよいでしょう。
デバッガでPosition確認
ストリームのPosition
プロパティは、現在の読み書き位置を示します。
EndOfStreamException
が発生する場合、Position
がLength
を超えているか、終端付近で不正な読み込みが行われている可能性が高いです。
デバッガを使って例外発生直前のPosition
をウォッチ式やローカル変数ウィンドウで確認してください。
読み込みループの中でPosition
がどのように変化しているかを追跡すると、どのタイミングで終端を超えたかがわかります。
また、Position
を手動で変更しているコードがあれば、その操作が正しいかどうかも検証してください。
特に、Seek
メソッドやPosition
の直接代入は誤った位置に設定されやすいため注意が必要です。
Stream.Length比較
Stream.Length
はストリーム全体のバイト数を示します。
Position
とLength
の比較は、読み込み可能な残りデータ量を把握するうえで欠かせません。
読み込み前にPosition
がLength
以上になっていないかをチェックすることで、終端超過の読み込みを防げます。
たとえば、BinaryReader
で固定長のデータを読み込む場合は、残りのバイト数が読み込みサイズ以上あるかを確認してから読み込みを行うのが安全です。
以下のような条件チェックが有効です。
if (stream.Length - stream.Position >= expectedReadSize)
{
// 読み込み処理
}
else
{
// 終端に達しているため読み込みを中止
}
このようにLength
とPosition
を比較して読み込み制御を行うことで、EndOfStreamException
の発生を未然に防げます。
ログ出力の配置
例外の原因を特定するためには、ストリームの読み込み処理周辺に適切なログ出力を配置することが効果的です。
ログには、読み込み前後のPosition
や読み込んだバイト数、読み込み要求サイズなどを記録します。
たとえば、読み込みループの開始時と終了時にPosition
をログに出力し、どの時点で終端を超えたかを追跡できるようにします。
また、例外発生時にはスタックトレースとともにPosition
やLength
の値をログに残すと、後から原因解析がしやすくなります。
ログ出力はSerilog
やNLog
などのロギングフレームワークを使うと便利です。
JSON形式で出力すれば、ログ解析ツールでの検索やフィルタリングも容易になります。
以下はログ出力例のイメージです。
[INFO] Read operation started. Position=1024, Length=2048, RequestedBytes=4
[INFO] Read operation completed. Position=1028
[ERROR] EndOfStreamException caught. Position=2047, Length=2048, RequestedBytes=4
このように詳細なログを残すことで、どの読み込み操作が終端超過を引き起こしたかを特定しやすくなります。
具体的な対策
読み込み前のEndOfStreamチェック
StreamReader.EndOfStream利用
StreamReader
を使ってテキストデータを読み込む場合、EndOfStream
プロパティを活用してストリームの終端に達しているかを事前に確認することが重要です。
これにより、終端を超えた読み込みを防ぎ、EndOfStreamException
の発生を抑制できます。
以下はStreamReader.EndOfStream
を使った安全な読み込み例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("sample.txt", FileMode.Open))
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
}
}
}
ファイルの各行が順に表示される
このコードでは、EndOfStream
がtrue
になるまでReadLine
を繰り返すため、終端を超えた読み込みが起きません。
ReadLine
の戻り値がnull
になるケースもありますが、EndOfStream
を使うことでより明示的に終端を検出できます。
BinaryReader.BaseStream.Position確認
バイナリデータをBinaryReader
で読み込む場合は、BaseStream.Position
とBaseStream.Length
を比較して、読み込み前に残りバイト数を確認することが効果的です。
これにより、読み込みサイズが残りデータを超えないように制御できます。
以下はBinaryReader
でのチェック例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
using (var reader = new BinaryReader(stream))
{
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
if (reader.BaseStream.Length - reader.BaseStream.Position >= 4)
{
int value = reader.ReadInt32();
Console.WriteLine($"読み込んだ値: {value}");
}
else
{
Console.WriteLine("残りデータが不足しているため読み込みを中止します。");
break;
}
}
}
}
}
読み込んだ値: 12345
読み込んだ値: 67890
残りデータが不足しているため読み込みを中止します。
このように、読み込み前に残りバイト数をチェックすることで、EndOfStreamException
を防げます。
期待サイズでループ制御
ストリームから複数のデータを連続して読み込む場合、期待するデータサイズに基づいてループを制御することが重要です。
例えば、固定長のレコードを読み込む場合は、残りのバイト数がレコードサイズ以上あるかを確認しながら読み込みを行います。
以下は固定長レコードを安全に読み込む例です。
using System;
using System.IO;
class Program
{
const int RecordSize = 8; // 例: 8バイトのレコード
static void Main()
{
using (var stream = new FileStream("records.bin", FileMode.Open))
using (var reader = new BinaryReader(stream))
{
while (reader.BaseStream.Length - reader.BaseStream.Position >= RecordSize)
{
byte[] record = reader.ReadBytes(RecordSize);
Console.WriteLine($"レコード読み込みです: {BitConverter.ToString(record)}");
}
Console.WriteLine("すべてのレコードを読み込みました。");
}
}
}
レコード読み込みです: 01-02-03-04-05-06-07-08
レコード読み込みです: 09-0A-0B-0C-0D-0E-0F-10
すべてのレコードを読み込みました。
この方法で、終端を超えた読み込みを防ぎつつ、正確にデータを処理できます。
Seekでストリーム先頭へ戻す
デシリアライズや複数回の読み込み処理を行う際、ストリームのPosition
が終端に近い場合は、先頭に戻すことで読み込みエラーを防げます。
Seek
メソッドやPosition
プロパティを使ってストリームの位置をリセットします。
以下はBinaryFormatter
でデシリアライズ前にストリーム位置を先頭に戻す例です。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class SampleData
{
public int Id;
public string Name;
}
class Program
{
static void Main()
{
var data = new SampleData { Id = 1, Name = "テスト" };
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
// 読み込み前にPositionを先頭に戻す
stream.Seek(0, SeekOrigin.Begin);
var deserialized = (SampleData)formatter.Deserialize(stream);
Console.WriteLine($"Id: {deserialized.Id}, Name: {deserialized.Name}");
}
}
}
Id: 1, Name: テスト
最新のC#ではBinaryFormatter
はセキュリティ上の問題から非推奨となっており、使用しないことが推奨されています(設定によってはコンパイルが通りません)。代わりに、例えばSystem.Text.Json
などのシリアライズ方法を使うのが良いです。
Position
を戻さずにデシリアライズを試みると、EndOfStreamException
が発生することがあります。
usingブロックでリソースを確実に閉じる
ストリームを使い終わったら、using
ブロックを使って確実にリソースを解放することが重要です。
リソースが解放されないと、ストリームの状態が不安定になり、読み込み位置の管理が乱れることがあります。
以下はusing
を使った安全なストリーム操作例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("sample.txt", FileMode.Open))
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
}
}
}
using
を使うことで、例外が発生してもストリームが確実に閉じられ、次回の読み込み時に不整合が起きにくくなります。
try-catchで例外を補足し再試行
EndOfStreamException
が発生する可能性がある処理は、try-catch
で囲み、例外を補足して適切に対処することができます。
場合によっては、読み込みを再試行したり、エラーメッセージをログに記録して処理を継続することも可能です。
以下は例外を補足して再試行する例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
using (var reader = new BinaryReader(stream))
{
bool success = false;
int retryCount = 0;
while (!success && retryCount < 3)
{
try
{
int value = reader.ReadInt32();
Console.WriteLine($"読み込んだ値: {value}");
success = true;
}
catch (EndOfStreamException)
{
retryCount++;
Console.WriteLine($"読み込み失敗。リトライ {retryCount} 回目");
stream.Seek(0, SeekOrigin.Begin);
}
}
if (!success)
{
Console.WriteLine("読み込みに失敗しました。");
}
}
}
}
読み込んだ値: 12345
この例では、例外発生時にストリームの位置を先頭に戻して再試行しています。
データフォーマット管理
ReadAsyncのキャンセル活用
非同期でストリームを読み込む場合、ReadAsync
メソッドにCancellationToken
を渡して読み込みをキャンセルできるように設計すると、途中で読み込みが長時間停止したり、終端を超えた読み込みを防止できます。
以下はReadAsync
でキャンセルトークンを使う例です。
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
{
byte[] buffer = new byte[1024];
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
Console.WriteLine($"読み込んだバイト数: {bytesRead}");
}
catch (OperationCanceledException)
{
Console.WriteLine("読み込みがキャンセルされました。");
}
}
}
}
キャンセルを活用することで、読み込みが長時間ブロックされることを防ぎ、例外発生のリスクを減らせます。
設計段階での契約定義
ストリームでやり取りするデータのフォーマットやサイズ、構造を設計段階で明確に定義し、契約(プロトコル)として文書化しておくことも重要です。
これにより、読み込み側は期待するデータサイズや構造を正確に把握でき、終端超過の読み込みを防止できます。
たとえば、ヘッダーにデータ長を含める、固定長レコードを使う、バージョン番号を付与するなどの設計が考えられます。
これらの契約に基づいて読み込み処理を実装すれば、EndOfStreamException
の発生を抑えられます。
リトライポリシー設定
ストリーム読み込み時に一時的な問題でEndOfStreamException
が発生する場合は、リトライポリシーを設定して再試行を行うことが有効です。
特にネットワークストリームや非同期処理で効果的です。
リトライ回数や待機時間を制御し、一定回数失敗したら処理を中断するようにします。
これにより、一時的な読み込み失敗を回避しつつ、無限ループを防止できます。
例外処理の共通化
EndOfStreamException
を含むストリーム関連の例外処理は、共通のハンドラやユーティリティメソッドにまとめると保守性が向上します。
共通化することで、例外発生時のログ出力やリトライ処理、リソース解放などを一元管理でき、コードの重複を減らせます。
たとえば、以下のような共通メソッドを用意します。
bool TryReadInt32(BinaryReader reader, out int value)
{
try
{
value = reader.ReadInt32();
return true;
}
catch (EndOfStreamException)
{
value = default;
// ログ出力やリトライ処理をここに記述
return false;
}
}
このように例外処理を共通化することで、コードの品質と信頼性を高められます。
コード例:良くない実装
ファイル全文読み込みでのミス
ファイルの全文を読み込む際に、終端チェックを怠るとEndOfStreamException
が発生しやすくなります。
以下のコードは、StreamReader
でファイルを読み込む際にEndOfStream
を確認せずにReadLine
を繰り返しているため、ファイル終端を超えて読み込みを試みてしまいます。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("sample.txt", FileMode.Open))
using (var reader = new StreamReader(stream))
{
while (true)
{
string line = reader.ReadLine();
// lineがnullかどうかのチェックをしていないため、
// ファイル終端を超えて読み込もうとして例外が発生する可能性がある
Console.WriteLine(line);
}
}
}
}
このコードは、ファイルの終端に達してReadLine
がnull
を返してもループを抜けず、null
をConsole.WriteLine
に渡すことで例外や予期しない動作を引き起こします。
EndOfStream
やReadLine
の戻り値を必ずチェックしないと、EndOfStreamException
が発生するリスクが高まります。
BinaryFormatter逆シリアライズ
BinaryFormatter
を使ったデシリアライズ時に、ストリームのPosition
を先頭に戻さずに読み込みを開始すると、ストリームの終端を超えて読み込もうとしてEndOfStreamException
が発生します。
以下のコードはその典型例です。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class SampleData
{
public int Id;
public string Name;
}
class Program
{
static void Main()
{
var data = new SampleData { Id = 1, Name = "テスト" };
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
// Positionを先頭に戻していないため、Deserializeで例外が発生する
var deserialized = (SampleData)formatter.Deserialize(stream);
Console.WriteLine($"Id: {deserialized.Id}, Name: {deserialized.Name}");
}
}
}
このコードは、Serialize
後のstream.Position
がストリームの終端にあるため、Deserialize
が読み込みを開始した時点で終端を超えてしまい、EndOfStreamException
がスローされます。
デシリアライズ前に必ずstream.Position = 0;
やstream.Seek(0, SeekOrigin.Begin);
で位置をリセットする必要があります。
受信バッファ固定長読み込み
ネットワークやファイルから固定長のデータを読み込む際に、残りのデータ量を確認せずに読み込みを行うと、終端を超えて読み込もうとしてEndOfStreamException
が発生します。
以下のコードは、BinaryReader
で4バイトの整数を読み込む際に残りバイト数をチェックしていない例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
using (var reader = new BinaryReader(stream))
{
while (true)
{
// 残りのバイト数を確認せずにReadInt32を呼び出しているため、
// ファイル終端に達するとEndOfStreamExceptionが発生する
int value = reader.ReadInt32();
Console.WriteLine($"読み込んだ値: {value}");
}
}
}
}
このコードは、ファイルの終端に達してもループを抜けずにReadInt32
を呼び続けるため、終端を超えた読み込みが発生し例外がスローされます。
読み込み前にreader.BaseStream.Length
とreader.BaseStream.Position
を比較して、残りバイト数が4バイト以上あるかを確認しないと安全に読み込めません。
コード例:改善実装
whileループとEndOfStream組み合わせ
StreamReader
を使ったテキストファイルの読み込みでは、EndOfStream
プロパティを活用してファイル終端を正しく検出しながらwhile
ループで読み込む方法が安全です。
これにより、終端を超えた読み込みを防ぎ、EndOfStreamException
の発生を回避できます。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("sample.txt", FileMode.Open))
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
}
}
}
ファイルの各行が順に表示される
このコードは、EndOfStream
がtrue
になるまでReadLine
を繰り返すため、ファイル終端を超えた読み込みを防止します。
ReadLine
の戻り値がnull
になるケースもありますが、EndOfStream
を使うことでより明示的に終端を検出できます。
async/awaitによる非同期読み込み
非同期処理でストリームを読み込む場合は、async
/await
を使い、StreamReader.ReadLineAsync
やStream.ReadAsync
を活用して効率的かつ安全に読み込みを行います。
非同期読み込みでも終端チェックを怠らないことが重要です。
以下はStreamReader.ReadLineAsync
を使った非同期読み込みの例です。
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (var stream = new FileStream("sample.txt", FileMode.Open))
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
string line = await reader.ReadLineAsync();
Console.WriteLine(line);
}
}
}
}
ファイルの各行が順に表示される
非同期読み込みはUIスレッドのブロックを防ぎ、パフォーマンス向上に寄与します。
EndOfStream
を使って終端を検出し、例外の発生を防いでいます。
Span<byte>を用いた安全な処理
Span<byte>
はスタック上に確保される軽量なバッファで、メモリ効率が高く安全にバイト列を扱えます。
ストリームからの読み込み時にSpan<byte>
を使うことで、バッファオーバーランや終端超過のリスクを減らせます。
以下はFileStream.Read
のオーバーロードでSpan<byte>
を使った例です。
using System;
using System.IO;
class Program
{
static void Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
{
Span<byte> buffer = stackalloc byte[1024];
int bytesRead;
while ((bytesRead = stream.Read(buffer)) > 0)
{
Console.WriteLine($"読み込んだバイト数: {bytesRead}");
// buffer.Slice(0, bytesRead)で有効データを扱う
}
}
}
}
読み込んだバイト数: 1024
読み込んだバイト数: 512
Span<byte>
を使うことで、バッファの境界を明確に管理でき、終端を超えた読み込みを防止しやすくなります。
PipeReader採用時の例
System.IO.Pipelines
のPipeReader
は、高性能なストリーム読み込みAPIで、非同期かつ効率的にデータを処理できます。
PipeReader
はバッファの終端を意識しながら読み込みを行うため、EndOfStreamException
の発生を抑制できます。
以下はPipeReader
を使った非同期読み込みの例です。
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
{
var reader = PipeReader.Create(stream);
while (true)
{
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
if (buffer.Length == 0 && result.IsCompleted)
{
break; // 終端に到達
}
// バッファ内のデータを処理(例: 全部読み捨て)
reader.AdvanceTo(buffer.End);
if (result.IsCompleted)
{
break;
}
}
await reader.CompleteAsync();
Console.WriteLine("読み込み完了");
}
}
}
読み込み完了
PipeReader
はバッファの終端を明示的に管理し、読み込み範囲を制御できるため、終端超過の読み込みによる例外を防ぎやすいです。
非同期処理との相性も良く、高速なストリーム処理に適しています。
ユニットテストによる再発防止
MSTestによる例外テスト
EndOfStreamException
の発生を防ぐためには、ユニットテストで例外が正しく発生するか、または発生しないかを検証することが重要です。
MSTestを使った例外テストでは、Assert.ThrowsException<T>
メソッドを利用して、特定の処理が例外をスローするかどうかを確認できます。
以下は、BinaryReader
で不正な読み込みを行いEndOfStreamException
が発生することをテストする例です。
using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class StreamTests
{
[TestMethod]
public void ReadBeyondEnd_ThrowsEndOfStreamException()
{
using (var stream = new MemoryStream(new byte[] { 0x01, 0x02 })) // 2バイトのみ
using (var reader = new BinaryReader(stream))
{
// 4バイト読み込もうとして例外が発生することを期待
Assert.ThrowsException<EndOfStreamException>(() =>
{
int value = reader.ReadInt32();
});
}
}
}
このテストは、2バイトしかないメモリストリームから4バイトの整数を読み込もうとした際にEndOfStreamException
が発生することを検証しています。
例外が発生しなければテストは失敗し、発生すれば成功となります。
このように例外発生を明示的にテストすることで、コードの安全性を高め、再発防止につなげられます。
NUnitとモックストリーム
NUnitを使う場合も同様に例外テストが可能です。
さらに、モックストリームを作成してストリームの動作を制御し、さまざまなシナリオをテストできます。
以下は、NUnitでEndOfStreamException
が発生するケースをモックストリームで再現し、例外を検証する例です。
using System;
using System.IO;
using NUnit.Framework;
public class MockStream : Stream
{
private readonly byte[] _data;
private long _position;
public MockStream(byte[] data)
{
_data = data;
_position = 0;
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_position >= _data.Length)
{
// ストリーム終端を超えた読み込みで例外をスロー
throw new EndOfStreamException("ストリームの終わりを超えて読み取ろうとしました。");
}
int bytesToRead = (int)Math.Min(count, _data.Length - _position);
Array.Copy(_data, _position, buffer, offset, bytesToRead);
_position += bytesToRead;
return bytesToRead;
}
// 必要な抽象メンバを実装(簡略化)
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _data.Length;
public override long Position { get => _position; set => throw new NotSupportedException(); }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
[TestFixture]
public class StreamTests
{
[Test]
public void MockStream_ReadBeyondEnd_ThrowsEndOfStreamException()
{
var data = new byte[] { 0x01, 0x02 };
using (var stream = new MockStream(data))
using (var reader = new BinaryReader(stream))
{
// 4バイト読み込みを試みて例外が発生することを検証
Assert.Throws<EndOfStreamException>(() =>
{
int value = reader.ReadInt32();
});
}
}
}
この例では、MockStream
を使って意図的に終端を超えた読み込みでEndOfStreamException
をスローするようにしています。
NUnitのAssert.Throws
で例外発生を検証し、テストの信頼性を高めています。
モックストリームを活用することで、実際のファイルやネットワークに依存せずに多様な読み込みシナリオを再現でき、例外処理の堅牢性を検証できます。
ログ設計と運用
Serilogで位置情報を記録
ストリーム操作で発生するEndOfStreamException
の原因特定には、読み込み位置やストリームの状態をログに記録することが非常に有効です。
Serilog
は柔軟な構成が可能なロギングライブラリで、ストリームのPosition
やLength
などの情報をログに含めることで、問題発生時の状況を詳細に把握できます。
以下は、Serilog
を使ってストリームの位置情報をログに記録する例です。
using System;
using System.IO;
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
using (var stream = new FileStream("data.bin", FileMode.Open))
using (var reader = new BinaryReader(stream))
{
try
{
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
Log.Information("読み込み開始 Position={Position} Length={Length}", reader.BaseStream.Position, reader.BaseStream.Length);
int value = reader.ReadInt32();
Log.Information("読み込み成功 Position={Position} Value={Value}", reader.BaseStream.Position, value);
}
}
catch (EndOfStreamException ex)
{
Log.Error(ex, "EndOfStreamException発生 Position={Position} Length={Length}", reader.BaseStream.Position, reader.BaseStream.Length);
}
}
Log.CloseAndFlush();
}
}
このコードでは、読み込み開始時と成功時にPosition
とLength
をログに出力し、例外発生時には例外情報とともに位置情報を記録しています。
これにより、どの位置で終端超過が起きたかを後から解析しやすくなります。
例外フィルタリング
大量のログが出力される環境では、EndOfStreamException
のような特定の例外だけを抽出して監視したい場合があります。
Serilog
ではフィルタリング機能を使い、特定の例外やメッセージをログレベルや条件で絞り込むことが可能です。
たとえば、EndOfStreamException
のみをエラーレベルでファイルに記録し、それ以外はコンソールに出力する設定例です。
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("errors.log", restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}",
filter: e => e.Exception is EndOfStreamException)
.CreateLogger();
この設定により、EndOfStreamException
だけがerrors.log
に記録され、他のログはコンソールに流れます。
これで例外の監視が効率化されます。
JSONログフォーマット
ログを機械的に解析・集約する場合、JSON形式でログを出力すると便利です。
Serilog
はJSONフォーマットのシンクを標準でサポートしており、ログ解析ツールやクラウドサービスと連携しやすくなります。
以下はJSON形式でログを出力する例です。
Log.Logger = new LoggerConfiguration()
.WriteTo.File(new Serilog.Formatting.Json.JsonFormatter(), "log.json")
.CreateLogger();
JSONログにはPosition
やLength
などのプロパティも含めて出力できるため、後から検索やフィルタリングが容易になります。
たとえば、EndOfStreamException
が発生した位置情報をキーにして問題の傾向分析が可能です。
アラート設定と監視
EndOfStreamException
はストリーム操作の異常を示す重要なシグナルなので、発生時に即座に対応できるようアラート設定を行うことが望ましいです。
ログを監視するツールやクラウドサービス(例:Azure Monitor、AWS CloudWatch、Elastic Stackなど)を使い、特定の例外が検出されたら通知を送る仕組みを構築します。
たとえば、Serilog
でJSONログを出力し、Elastic Stack(Elasticsearch + Kibana + Logstash)に連携して、EndOfStreamException
のログが一定回数を超えたらメールやSlackに通知するルールを設定できます。
また、ログのリアルタイム監視ツールを使い、例外発生時にダッシュボードでアラートを表示したり、運用チームに自動通知することで迅速な対応が可能です。
このように、ログ設計と運用においては、単にログを残すだけでなく、例外のフィルタリングやフォーマット、監視・アラート体制を整えることが、EndOfStreamException
の早期発見と再発防止に役立ちます。
パフォーマンスとメモリ考慮
バッファリング最適化
ストリームからの読み込み処理において、バッファリングの最適化はパフォーマンス向上とメモリ効率の改善に直結します。
適切なバッファサイズを設定することで、I/O回数を減らし、CPU負荷やディスクアクセスのオーバーヘッドを抑えられます。
.NETのFileStream
やBufferedStream
はバッファリング機能を持っており、デフォルトのバッファサイズは一般的に4KBや8KB程度ですが、用途に応じて調整が可能です。
大きすぎるバッファはメモリ消費を増やし、小さすぎるバッファはI/O回数が増えてパフォーマンス低下を招きます。
以下はFileStream
のバッファサイズを指定して読み込む例です。
using System;
using System.IO;
class Program
{
static void Main()
{
const int bufferSize = 64 * 1024; // 64KBのバッファサイズを指定
using (var stream = new FileStream("largefile.bin", FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize))
{
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// 読み込んだデータを処理
Console.WriteLine($"読み込んだバイト数: {bytesRead}");
}
}
}
}
読み込んだバイト数: 65536
読み込んだバイト数: 65536
...
この例では64KBのバッファを使い、I/O回数を減らして効率的に読み込んでいます。
バッファサイズはファイルサイズやシステムのメモリ状況に応じて調整してください。
また、BufferedStream
を使うことで、既存のストリームにバッファリング機能を追加できます。
特にネットワークストリームや圧縮ストリームなど、バッファリングがないストリームに対して有効です。
using (var fileStream = new FileStream("data.bin", FileMode.Open))
using (var bufferedStream = new BufferedStream(fileStream, bufferSize))
{
// bufferedStreamを使って読み込み処理
}
バッファリングの最適化は、EndOfStreamException
の防止には直接関係しませんが、読み込み処理の安定性と効率を高めるために重要なポイントです。
大容量ファイルの分割読み込み
大容量ファイルを一度に読み込むとメモリ消費が増大し、パフォーマンス低下やシステムの不安定化を招く恐れがあります。
これを防ぐために、ファイルを分割して部分的に読み込む方法が推奨されます。
分割読み込みでは、ファイルのサイズやレコード単位で読み込み範囲を制御し、必要な部分だけを処理します。
これによりメモリ使用量を抑えつつ、効率的にデータを扱えます。
以下は大容量ファイルをチャンク単位(例:1MB)で分割読み込みする例です。
using System;
using System.IO;
class Program
{
static void Main()
{
const int chunkSize = 1024 * 1024; // 1MB
byte[] buffer = new byte[chunkSize];
using (var stream = new FileStream("largefile.bin", FileMode.Open))
{
long totalSize = stream.Length;
long offset = 0;
while (offset < totalSize)
{
int bytesToRead = (int)Math.Min(chunkSize, totalSize - offset);
int bytesRead = stream.Read(buffer, 0, bytesToRead);
if (bytesRead == 0)
{
break; // ファイル終端に到達
}
// 読み込んだチャンクを処理
Console.WriteLine($"チャンク読み込みです: {bytesRead} バイト, オフセット: {offset}");
offset += bytesRead;
}
}
}
}
チャンク読み込みです: 1048576 バイト, オフセット: 0
チャンク読み込みです: 1048576 バイト, オフセット: 1048576
...
この方法では、メモリに負荷をかけずに大容量ファイルを安全に処理できます。
分割読み込みは、EndOfStreamException
の発生を防ぐためにも有効で、読み込み範囲を明確に制御できるため、終端超過の読み込みを回避しやすくなります。
また、分割読み込み時は、チャンクの境界でデータの切れ目がないか注意が必要です。
レコード単位のファイルの場合は、チャンクの最後で不完全なレコードが切れていないか検証し、必要に応じて次のチャンクと結合して処理する工夫が求められます。
よくあるQ&A
非同期メソッドでの挙動
非同期メソッドを使ってストリームからデータを読み込む場合、EndOfStreamException
の発生は同期処理と基本的に同じ原因で起こります。
つまり、ストリームの終端を超えて読み込みを試みたときに例外がスローされます。
ただし、非同期処理特有の注意点もあります。
非同期読み込みでは、ReadAsync
やReadLineAsync
などのメソッドを使い、await
で結果を待ちます。
読み込み完了までの待機中にキャンセルやタイムアウトが発生すると、OperationCanceledException
やTimeoutException
がスローされることがありますが、EndOfStreamException
は読み込み範囲の超過が原因で発生します。
非同期メソッドでのEndOfStreamException
を防ぐには、読み込み前にStream.Length
やPosition
を確認し、残りのデータ量を把握してから読み込みを行うことが重要です。
また、ReadAsync
の戻り値である読み込んだバイト数が0の場合は終端に達したことを示すため、これを検出して読み込みループを終了させる設計が推奨されます。
以下は非同期読み込みで終端を正しく検出する例です。
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (var stream = new FileStream("data.bin", FileMode.Open))
{
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Console.WriteLine($"読み込んだバイト数: {bytesRead}");
}
Console.WriteLine("ファイル終端に到達しました。");
}
}
}
このように、非同期でも読み込みバイト数のチェックを行い、終端を超えた読み込みを防ぐことが重要です。
Unity環境での注意点
Unity環境でEndOfStreamException
が発生する場合、特に注意すべきポイントがいくつかあります。
Unityは独自のランタイムやAPIを持ち、マルチプラットフォーム対応のためにストリームの挙動が標準の.NET環境と異なることがあります。
まず、UnityのFileStream
やBinaryReader
は基本的に.NETの実装に準じていますが、プラットフォームによってはファイルアクセスの制限や非同期処理のサポート状況が異なります。
特にモバイルやWebGL環境ではファイルシステムの制約が厳しく、ストリームの終端検出が不安定になることがあります。
また、Unityのコルーチンやasync
/await
を使った非同期処理では、ストリームの読み込み中にフレーム更新が行われるため、読み込み状態の管理が複雑になります。
読み込み途中でストリームが閉じられたり、Position
が不正に変更されるとEndOfStreamException
が発生しやすくなります。
対策としては、以下の点に注意してください。
- ファイルアクセスは可能な限り同期的に行い、読み込み完了後に処理を進める
- 非同期処理を使う場合は、読み込み状態を明確に管理し、
Position
やLength
を適切にチェックします - プラットフォーム固有のファイルアクセス制限を理解し、必要に応じてUnityの
StreamingAssets
やResources
フォルダを活用します - ファイルの存在チェックやサイズ確認を事前に行い、不完全なファイル読み込みを防ぐ
これらの注意点を守ることで、Unity環境でのEndOfStreamException
の発生を抑制できます。
.NET Frameworkとの違い
EndOfStreamException
の挙動自体は、.NET Frameworkと.NET Core/.NET 5以降で大きな違いはありませんが、ストリームAPIの内部実装やパフォーマンス、非同期処理のサポート状況に差異があります。
.NET Frameworkでは、非同期ストリーム読み込みのAPIが限定的であり、ReadAsync
やReadLineAsync
の実装も古いバージョンでは最適化が不十分な場合があります。
そのため、非同期処理中に例外が発生しやすいケースや、終端検出のタイミングが微妙に異なることがあります。
一方、.NET Coreや.NET 5以降では、ストリームAPIが大幅に改善され、非同期処理のパフォーマンスや安定性が向上しています。
PipeReader
などの新しいAPIも利用可能で、終端検出やバッファ管理がより厳密に行われるため、EndOfStreamException
の発生を抑制しやすくなっています。
また、.NET FrameworkではBinaryFormatter
が標準的に使われていましたが、セキュリティ上の理由から.NET Core以降では推奨されておらず、代替のシリアライズ手法が推奨されています。
これに伴い、シリアライズ・デシリアライズ時のEndOfStreamException
の発生パターンも変わる可能性があります。
まとめると、.NET Frameworkと最新の.NET環境ではAPIの進化により、ストリーム操作の安定性や例外発生の抑制に差があるため、開発環境に応じた対策やAPI選択が重要です。
まとめ
この記事では、C#で発生するEndOfStreamException
の原因や典型的な発生パターン、具体的な対策方法を詳しく解説しました。
ストリームの位置管理や終端検出の重要性、適切な読み込み制御、非同期処理での注意点などを理解できます。
また、ユニットテストやログ設計による再発防止策も紹介し、安定したストリーム操作の実現に役立つ知識を提供しています。