.NET Framework 4.0でBinaryReaderのストリームを閉じない方法|leaveOpen相当をNonClosingStreamで実現【VB/C#実装・テスト付き】

古い .NET Framework 4.0 では、BinaryReader に leaveOpen 引数がなく、Using ブロックや Close() を呼ぶと基底ストリームまで閉じられてしまいます。本記事では、アプリを 4.x 以降へ上げられない環境でも安全・簡潔に「ストリームは開いたまま BinaryReader だけを破棄」できる汎用ラッパーの実装と、導入時の注意点・テスト・ベンチマークの考え方まで丁寧に解説します。

目次

.NET Framework 4.0 環境で直面する課題

BinaryReader は内部に保持した Stream(以下 InputStream)を「所有」している前提で設計されているため、4.0 環境では Using を抜ける/Close() を呼ぶと BinaryReader のみならず InputStream も Dispose されます。続けて同じストリームを読み直したい・別の処理に引き継ぎたいといった要件では、これが致命的な制約になります。

結論:NonClosingStream ラッパーで leaveOpen 相当 を実現する

解決策はシンプルです。内側のストリームを閉じないことだけを目的としたパススルー(委譲)型の Stream を自作し、BinaryReader からはそのラッパーを渡します。ラッパー自身は破棄されても、内側の InputStream には一切触れません。

実装(VB.NET):NonClosingStream

'InputStream を包むだけのパススルー・ストリーム(NotInheritable で固定化)
Public NotInheritable Class NonClosingStream
    Inherits Stream


Private ReadOnly inner As Stream

Public Sub New(s As Stream)
    If s Is Nothing Then Throw New ArgumentNullException(NameOf(s))
    inner = s
End Sub

'====== 通常の委譲 ======
Public Overrides ReadOnly Property CanRead As Boolean
    Get
        Return inner.CanRead
    End Get
End Property

Public Overrides ReadOnly Property CanSeek As Boolean
    Get
        Return inner.CanSeek
    End Get
End Property

Public Overrides ReadOnly Property CanWrite As Boolean
    Get
        Return inner.CanWrite
    End Get
End Property

Public Overrides ReadOnly Property Length As Long
    Get
        Return inner.Length 'サポートしていない場合は内側が例外を投げる
    End Get
End Property

Public Overrides Property Position As Long
    Get
        Return inner.Position
    End Get
    Set(ByVal value As Long)
        inner.Position = value
    End Set
End Property

Public Overrides Sub Flush()
    inner.Flush()
End Sub

Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
    Return inner.Read(buffer, offset, count)
End Function

Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
    inner.Write(buffer, offset, count)
End Sub

Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
    Return inner.Seek(offset, origin)
End Function

Public Overrides Sub SetLength(value As Long)
    inner.SetLength(value)
End Sub

'====== ここが肝心:自分は閉じるが inner は絶対に閉じない ======
Protected Overrides Sub Dispose(disposing As Boolean)
    'BinaryReader/Writer などから Close/Dispose されても内側は触らない
    MyBase.Dispose(False) '自分自身の管理対象(バッファ等)があれば解放。inner は解放しない
End Sub


End Class 

使い方(VB.NET)

Dim data As Byte() = {&H01, &H02, &H03, &H04}
Using input As New MemoryStream(data, writable:=False)
    'NonClosingStream に包んで BinaryReader に渡す
    Using br As New BinaryReader(New NonClosingStream(input), Encoding.ASCII)
        Dim b1 As Byte = br.ReadByte()
        Dim b2 As Byte = br.ReadByte()
        '... 読み取り処理 ...
    End Using '← br は解放されるが input は開いたまま
    'ここで input は引き続き使用できる
    input.Seek(0, SeekOrigin.Begin)
    Dim again As Integer = input.ReadByte() '0x01 を再び読み取れる
End Using

実装(C#)版も用意

public sealed class NonClosingStream : Stream
{
    private readonly Stream _inner;
    public NonClosingStream(Stream inner)
    {
        _inner = inner ?? throw new ArgumentNullException(nameof(inner));
    }


// ===== 委譲 =====
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
public override long Length => _inner.Length;

public override long Position
{
    get => _inner.Position;
    set => _inner.Position = value;
}

public override void Flush() => _inner.Flush();
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void SetLength(long value) => _inner.SetLength(value);

// ===== ここが肝心 =====
protected override void Dispose(bool disposing)
{
    // 内側は絶対に閉じない
    base.Dispose(false);
}


} 

使い方(C#)

byte[] data = { 0x01, 0x02, 0x03, 0x04 };
using (var input = new MemoryStream(data, writable: false))
{
    using (var br = new BinaryReader(new NonClosingStream(input), Encoding.ASCII))
    {
        var b1 = br.ReadByte();
        var b2 = br.ReadByte();
        // ... 読み取り処理 ...
    } // br は破棄されるが input は開いたまま
    input.Seek(0, SeekOrigin.Begin);
    int again = input.ReadByte(); // 0x01 を再び取得
}

なぜ内側が閉じられないのか(Dispose/Close の仕組み)

BinaryReader は Dispose(True)(=明示的な破棄)時に保持中の Stream を閉じます。そこで、破棄シーケンスに「内側へ触らない」層を挟むことで、BinaryReader はラッパーだけを閉じ、InputStream まで到達しません。読み取り・書き込みの実体は常に inner に委譲されるため、機能面の副作用も極めて限定的です。

既存コードの導入が簡単

変更箇所BeforeAfter
BinaryReader の生成New BinaryReader(InputStream, enc)New BinaryReader(New NonClosingStream(InputStream), enc)
Using ブロック抜けると InputStream も閉じる抜けても InputStream は開いたまま
既存資産への影響大小(生成箇所だけ差し替え)

ポイントと運用上のチェック

  • 責務の分離: BinaryReader の破棄と InputStream の寿命管理を分離できます。内側を閉じるタイミングは呼び出し側が明示的に決めてください。
  • 例外時も安全: 例外で Using を抜けても、BinaryReader だけが確実に破棄され、InputStream は保持されます。
  • オーバーヘッド: 委譲 1 回分のメソッド呼び出しのみ。実測でも誤差レベルのオーバーヘッドに収まるケースがほとんどです。
  • 非対応メンバー: 一部ストリームは Length や Seek をサポートしません。その場合は内側が投げる例外をそのまま伝搬します。
  • スレッド安全性: もとより Stream はスレッドセーフではありません。並行アクセスは排他制御を。

表:アプローチ比較と推奨度

手段概要利点欠点推奨度
NonClosingStream(本記事)内側を閉じないラッパーを渡す安全・簡潔・既存改修が少ないラッパーの小さなオーバーヘッド◎
BinaryReader を破棄しない明示的に Dispose を呼ばない実装が楽例外時にリソース解放されず危険/静的解析で警告×(非推奨)
BinaryReader を継承して Dispose を変更Dispose の挙動を上書き追加ラッパー不要実装依存度が高く将来互換性リスク△(避けられるなら回避)

読み直し・再利用が必要な場合のコツ

InputStream を引き続き利用したい場合は、位置を巻き戻します。

InputStream.Seek(0, SeekOrigin.Begin) '先頭から読ませたいとき

読み取りが終わった時点の Position を保存しておけば、必要な箇所へ素早くジャンプできます。

ユニットテスト例(VB.NET / MSTest)

<TestClass>
Public Class NonClosingStreamTests
    <TestMethod>
    Public Sub BinaryReader_Dispose_DoesNotCloseInner()
        Dim bytes = New Byte() {&H10, &H20, &H30}
        Using inner As New MemoryStream(bytes, writable:=False)
            Using br As New BinaryReader(New NonClosingStream(inner), Encoding.ASCII)
                Assert.AreEqual(&H10, br.ReadByte())
            End Using 'BinaryReader を破棄
            'inner がまだ開いていることを確認(読み出し可能)
            Dim remain As Integer = inner.ReadByte()
            Assert.AreEqual(&H20, remain)
        End Using
    End Sub
End Class

簡易ベンチマークの目安(C#)

var data = new byte[1024 * 1024];
new Random(0).NextBytes(data);

var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < 100; i++)
{
using (var ms = new MemoryStream(data))
using (var br = new BinaryReader(new NonClosingStream(ms), Encoding.UTF8))
{
int sum = 0;
for (int j = 0; j < data.Length; j++) sum += br.ReadByte();
}
}
sw.Stop();
Console.WriteLine(sw.Elapsed); // 参考:ラッパー有のオーバーヘッドは多くのケースで無視可能 

BinaryWriter / StreamReader でも同じパターン

「内側を閉じない」ことが目的なら、書き込み系・テキスト系でも同じ設計で解決できます。

'BinaryWriter でファイルストリームを開いたままにする例
Using fs As New FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read)
    Using bw As New BinaryWriter(New NonClosingStream(fs), Encoding.UTF8)
        bw.Write(&H2A) '書き込み
    End Using '← fs は開いたまま
    fs.Position = 0 'すぐ読み返す等の連携が可能
End Using
// StreamReader でも同様
using (var s = File.OpenRead(path))
using (var sr = new StreamReader(new NonClosingStream(s), Encoding.UTF8))
{
    string line = sr.ReadLine();
}
// ここでも s は有効

堅牢性を高める追加実装(任意)

  • 防御的プログラミング: コンストラクタで Nothing/null を明示的に弾く。
  • 所有フラグの一般化: 将来の再利用を見据え、ownsInner As Boolean を持つ OwnershipStream として設計しても良い(今回は用途が明確なので割愛)。
  • デバッグ支援: ToString() をオーバーライドして内側の型名やハンドル状態を出力すると解析が楽です。
  • IDisposable の多重呼び出し: .NET の規約どおり多重 Dispose() は無害に。上記実装は idempotent です。

セキュリティと例外処理の観点

  • 例外時のリソースリーク防止: ラッパーを挟めば、BinaryReader は必ず解放され、ストリーム所有権は呼び出し側で明示的に管理できます。
  • ファイルロックの管理: ファイルストリームを開いたまま後続処理に回す場合、FileShare の指定を正しく。想定外のロック保持を避けるため、利用完了後は InputStream.Dispose() を必ず実施。
  • 扱うデータの信頼性: バイナリ形式の破損やオフセット不正を検知するため、読み取り前に十分な長さやシグネチャを確認しましょう。

トラブルシューティング

症状原因の可能性対処
後続処理で「ストリームが閉じられています」ラッパーを挟まずに直接渡している箇所が混在生成箇所を総点検し、NonClosingStream の挿入漏れを解消
NotSupportedException が出る内側が Seek/Length 非対応のストリーム巻き戻しを前提にしない設計に変更 or BufferedStream 等で吸収
パフォーマンス低下が疑われる小刻みな ReadByte() の多用バッファ単位の Read() へ切替、まとめ読みで往復回数を削減

ミニマム導入チェックリスト

  • すべての BinaryReader 生成箇所で new NonClosingStream(...) を挟んだか。
  • 後続で InputStream を確実に Dispose() するポイントを設けたか。
  • 巻き戻しが必要なケースで Seek(0, Begin) 等を忘れていないか。
  • 例外時も資源解放が漏れないよう Using を徹底したか。

サンプル:バイナリヘッダーを読み捨てて本体を別パーサへ

Public Sub Parse(ByVal input As Stream)
    'ヘッダーだけ BinaryReader で読み、ストリームは次の処理へ渡す
    Using br As New BinaryReader(New NonClosingStream(input), Encoding.UTF8)
        Dim magic = br.ReadUInt32() '仮想ヘッダー
        Dim version = br.ReadUInt16()
        '... ヘッダー検証 ...
    End Using '← ここで BinaryReader は破棄される
    '本体は別のコンポーネントへ(input は生きている)
    Call ParseBody(input)
End Sub

保守の観点:コード規約と静的解析

  • 規約化: 「BinaryReader/Writer/StreamReader/Writer を使うときは、所有権が呼び出し側にあるなら必ず NonClosingStream を挟む」をチーム規約に。
  • 静的解析: IDisposable の未破棄警告は原則解消。ラッパー適用後も InputStream の最終 Dispose() は別途必須。

まとめ

.NET Framework 4.0 には BinaryReader(..., leaveOpen:=True) が存在しないため、そのままでは BinaryReader の破棄と同時に基底ストリームも閉じられてしまいます。本記事の NonClosingStream を導入すれば、leaveOpen 相当 の動作を安全に実現し、既存コードの改修も最小限に抑えられます。あとは「ストリームの寿命は呼び出し側が責任を持って閉じる」という原則だけ守れば、ヘッダー解析→本体処理の分離、複数パーサへの受け渡し、再読込など、4.0 環境でも柔軟なストリーム運用が可能になります。


FAQ

Q. ラッパーを 2 重に入れても大丈夫?
A. ほぼ問題ありませんが、読み取り回数・例外経路の可観測性が落ちるのでデバッグはしづらくなります。基本は 1 回で十分です。

Q. Dispose(False) を呼んでいるのはなぜ?
A. ラッパー自身が仮に持つマネージ資源のみを解放し、内側(inner)へは一切触れない意思を明示するためです。base.Dispose(False)/MyBase.Dispose(False) は、この目的に合致します。

Q. 既存の外部ライブラリに渡すときも使える?
A. はい。Stream を受け取るあらゆる API に対して、NonClosingStream を挟むだけで「所有権を奪わせない」ことができます。

実運用テンプレート(VB.NET)

'共通ヘルパー
Public Function CreateBinaryReaderLeaveOpen(s As Stream, enc As Encoding) As BinaryReader
    Return New BinaryReader(New NonClosingStream(s), enc)
End Function

'利用側
Using input As Stream = OpenYourStreamSomehow()
Using br As BinaryReader = CreateBinaryReaderLeaveOpen(input, Encoding.UTF8)
'... 読み取り ...
End Using '← input は引き続き利用可能
'ここで必要に応じて input を明示的に閉じる
input.Dispose()
End Using 

品質保証のための最終チェック

  • ユニットテストで「BinaryReader 破棄後に InputStream が有効であること」をシナリオ別に検証。
  • 例外経路(途中でフォーマット不正/EOF)でも InputStream を閉じないことを確認。
  • 長時間稼働でファイルハンドルが枯渇しない(最終的に InputStream を閉じている)ことを監視。

おわりに

ラッパーの追加だけで、古いフレームワークの制限を上手く回避しつつモダンな所有権管理を取り戻せます。NonClosingStream は 1 ファイルで完結する軽量パターンなので、レガシー環境の保守・トラブル対応の「まずはここから」の手当として、ぜひ組織標準に取り入れてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次