BinaryReaderクラスおよびBinaryWriterクラスはStreamに対してバイナリデータの読み書きを行うためのクラスです。 Stream単体ではバイト配列での読み書きしか行えませんが、StreamとBinaryReader・BinaryWriterを組み合わせて使うことで構造化されたバイナリデータの読み書きが可能になります。
BinaryReader・BinaryWriterでは、読み書きの際のバイトオーダにリトルエンディアンを使用します。 実行環境のバイトオーダによらず、常にリトルエンディアンでの読み書きが行われます。
なお、.NET Frameworkにはビッグエンディアンで読み書きを行うBinaryReader・BinaryWriterは用意されていません。 BinaryReader・BinaryWriterが使用するバイトオーダを指定したり変更することもできません。 ビッグエンディアンでの読み書きを行うには、バイトオーダの変換などを自前で実装する必要があります。 この点については、ランタイム・システム・プラットフォームの情報 §.エンディアンや基本型の型変換 §.基本型とバイト配列への/からの変換などを参照してください。
BinaryReader
基本型の読み込み (ReadInt32, etc)
BinaryReaderには、基本型の読み込みを行うメソッドがいくつか用意されています。
BinaryReaderに用意されているメソッドと、読み込める基本型は次のとおりです。 ほとんどの.NET Frameworkのプリミティブ型に対応する読み込みメソッドが用意されています。
メソッド | 読み込めるデータ型 | 読み込まれるデータのサイズ |
---|---|---|
ReadSByte | sbyte, SByte | 1バイト |
ReadInt16 | short, Short | 2バイト |
ReadInt32 | int, Integer | 4バイト |
ReadInt64 | long, Long | 8バイト |
ReadByte | byte, Byte | 1バイト |
ReadUInt16 | ushort, UShort | 2バイト |
ReadUInt32 | uint, UInteger | 4バイト |
ReadUInt64 | ulong, ULong | 8バイト |
ReadSingle | float, Single | 4バイト |
ReadDouble | double, Double | 8バイト |
ReadDecimal | decimal, Decimal | 16バイト |
ReadChar | char, Char | 2バイト |
ReadBoolean | bool, Boolean | 4バイト |
メソッド | 読み込めるデータ型 | 読み込まれるデータのサイズ |
すでに述べた通り、これらのメソッドで複数バイトのデータを読み込む場合、リトルエンディアンで読み込まれます。
BinaryReaderにはビット単位での読み込みを行うメソッドは用意されていません。
BinaryReaderにはDateTimeやDateTimeOffsetを読み込むメソッドも用意されていません。 DateTime・DateTimeOffsetの読み書きを行う場合はDateTime.ToBinary・DateTime.FromBinaryなどのメソッドを使って数値として扱う必要があります。
これらのメソッドでストリームの残りバイト数よりも多いバイト数を読み込もうとした場合(読み込むことによってストリームの末尾を超えてしまう場合)には、例外EndOfStreamExceptionがスローされます。 この際、読み込みに失敗してもストリームの現在位置は読み込む前の位置には戻りません。
バイト配列の読み込み (ReadBytes, Read)
ストリームから複数バイトを読み込みバイト配列として取得するにはReadBytesメソッドを使うことができます。 このメソッドでは、読み込みたいバイト数を引数に指定します。
上記の例で使用しているBitConverter.ToStringメソッドはバイト配列を見やすい形式に変換するためのもので、ReadBytesメソッドを使った読み込み処理の本質とは無関係のものです。
ReadBytesメソッドでは、ストリームの残りバイト数よりも多いバイト数を読み込もうとした場合には実際に読み込めた分のみが返されます。 読み込み中にストリームの末尾に達した場合でもEndOfStreamExceptionはスローされません。 実際に読み込めたバイト数は返されるバイト配列の長さを調べることで知ることができます。 例えば、16バイト読み込もうとして実際には残り8バイトだった場合には、長さ8のバイト配列が返されます。 ストリームの末尾に達してそれ以上読み込めるデータがない場合、ReadBytesメソッドは長さ0のバイト配列を返します。
もうひとつ、バイト配列に読み込むメソッドとしてReadメソッドも用意されています。 こちらはReadBytesメソッドとは異なり、あらかじめ用意したバイト配列を指定することにより、読み込んだデータをその配列へ格納します。 Readメソッドは、Stream.Readメソッドと同様に指定したバイト数の分だけ読み込みを試みますが、指定したバイト数ちょうどのデータが一度に読み込まれるとは限りません。 読み込み中にストリームの末尾に達した場合でもEndOfStreamExceptionはスローされません。 実際に読み込めたバイト数は戻り値で知ることができます。 ストリームの末尾に達していてそれ以上読み込めるデータがない場合、Readメソッドは0を返します。
BinaryReaderにはStreamReader.ReadToEndメソッドのようなストリームの末尾までをひとつのバイト配列に読み込むメソッドは用意されていないため、必要な場合は自前で実装する必要があります。 次の例は、そのようなメソッドを実装した例です。 効率的な実装ではないため、あくまで参考程度のものです。
上記のサンプル中で使用しているArraySegmentについては部分配列 §.ArraySegment構造体、Buffer.BlockCopyメソッドについてはバイト列操作 §.BlockCopyメソッドを参照してください。
なお、読み込み元がファイルに限定される場合はFileクラスのメソッドを使うこともできます。 File.ReadAllBytesメソッドを使うと、BinaryReaderを使わずにファイルの内容をバイト配列として読み込むことができます。
文字列の読み込み (ReadString)
ストリームから文字列を読み込むにはReadStringメソッドを使うことができます。
ReadStringメソッドは、BinaryWriter.Writeメソッドで書き込まれる形式での読み込みを行います。 BinaryWrite.Writeメソッドで文字列を書き込む場合、先頭に文字列の長さが書き込まれます。 ReadStringメソッドはその長さを元に文字列の読み込みを行います。
従ってReadStringメソッドでは、読み込む文字列の長さを事前に知っている必要はなく、引数で読み込む文字列の長さを指定する必要もありません。 逆に、ReadStringメソッドを固定長の文字列フィールドを読み込む目的に使用することはできません。 固定長の文字列フィールドの読み込みを行う場合は、ReadBytesメソッドやReadCharsメソッドを使って読み込み処理を記述する必要があります。 固定長の文字列フィールドを読み書きする具体例はBinaryWriter.Writeメソッドの解説をご覧ください。
ReadStringメソッドで文字列を読み込む際、デフォルトではUTF-8でデコードして読み込みますが、BinaryReaderのコンストラクタで目的のEncodingを指定することで任意のエンコーディングで文字列を読み込むことができます。 当然、BinaryWriter.Writeメソッドで書き込んだときと同じエンコーディングを指定しないと正しくデコードされません。
文字配列の読み込み (ReadChars)
ストリームから複数バイトを読み込み文字配列(char[])として取得するにはReadCharsメソッドを使うことができます。 このメソッドはReadBytesメソッドと似ていますが、引数にはバイト数ではなく読み込む文字数を指定します。 したがって、実際に読み込まれるバイト数はBinaryReaderが使用するエンコーディングによって変わります。
ReadCharsメソッドを使うことでストリームから固定長の文字列フィールドを読み込むことができます。 固定長の文字列フィールドを読み書きする具体例はBinaryWriter.Writeメソッドの解説をご覧ください。
ReadStringメソッドの場合と同様、ReadCharメソッドはBinaryReaderのコンストラクタで指定したエンコーディングでのデコードを行います。 デフォルトではUTF-8でエンコードされているものとして読み込まれます。
文字単位での読み込み・先読み (Read, PeekChar)
Readメソッドを引数なしで呼び出すと、ストリームから1文字(char)を読み込みます。 ReadCharメソッドと似ていますが、戻り値はint/Integerで、ストリームの末尾に達した場合は-1が返されます。 そのため、ストリームの末尾に達した状態でこのメソッドを呼び出しても例外EndOfStreamExceptionはスローされません。
ReadStringメソッドの場合と同様、ReadメソッドはBinaryReaderのコンストラクタで指定したエンコーディングでのデコードを行います。 デフォルトではUTF-8でエンコードされているものとして読み込まれます。
PeekCharメソッドを使うと、次の1文字を先読みすることができます。 Peekメソッドの戻り値は引数なしのReadメソッドと同様です。
シーク
BinaryReaderにはストリームのシークを行うメソッドは用意されていません。 シークを行うには、まずBaseStreamプロパティを参照してベースとなっているStreamを取得し、そして取得したStreamのSeekメソッドを呼び出すようにします。
BinaryReaderでは内部でデータの先読みとバッファリングが行われるようになっているため、BaseStreamを参照してストリームのシークを行うと読み込まれる内容に不整合が起こる可能性が考えられます。 実際にそういったことが起こるかどうかはドキュメントには明記されていないため不明確ですが、BinaryReaderにSeekメソッドが用意されていない点を勘案すると、BinaryReaderでランダムアクセスを行うのは避けたほうがよいと思われます。
データを読み飛ばす目的でシークを行いたい場合には、シークを行うかわりにReadBytes等のメソッドを使ってシークしたい分だけデータを読み込み、その戻り値は単に破棄する、といった方法をとることができます。 この方法の場合、ストリーム後方へのシークのみしかできないものの、操作によってバッファリングされている内容に不整合が起きることはないため、確実な方法と言えます。
BinaryWriter
基本型の書き込み
BinaryWriterで書き込みを行う場合はWriteメソッドを使います。 このメソッドでは与えられた引数の型に従って内容が書き込まれます。 Writeメソッドで基本型の書き込みを行う際、数値リテラルを直接指定して書き込む場合は、リテラルにサフィックスをつけたり明示的にキャストすることにより、書き込もうとしている値の型を明確にすることをおすすめします。
Writeメソッドで書き込める基本型の種類はReadメソッドと同じです。 WriteメソッドではDateTimeやDateTimeOffsetを直接書き込むことはできないので、ToBinary/FromBinaryなどのメソッドを使って数値などに変換してから書き込む必要があります。
バイト配列の書き込み
Writeメソッドではバイト配列の書き込みにも対応しています。 引数に指定する値と意味はStream.Writeメソッドと同じです。 BinaryWriter.Writeメソッドでは、バイト配列のみを指定することでその内容すべてを書き込むようにすることもできます。
なお、書き込み先がファイルに限定される場合はFileクラスのメソッドを使うこともできます。 File.WriteAllBytesメソッドを使うと、BinaryWriterを使わずにバイト配列をファイルに書き込むことができます。
文字列の書き込み
Writeメソッドは文字列の書き込みにも対応しています。
BinaryWriterで文字列を書き込む場合、まず文字列の長さがストリームに書き込まれ、続けて文字列のバイト表現が書き込まれます。 これにより、読み込みの際に文字列の具体的な長さを知らなくても任意の長さの文字列を読み込むことができるようになっています。 実際、文字列を読み込むBinaryReader.ReadStringメソッドには引数で読み込む文字列の長さを指定することはできません。
上記の実行結果における1バイト目の 0x08 は後続する文字列のバイト数が 8 であることを表しています。 BinaryReader.ReadStringメソッドのドキュメントによると、文字列とその長さは次のように書き込まれます。
現在のストリームから 1 つの文字列を読み取ります。ストリームの文字列は、7 ビットごとにエンコードされた文字列の長さが先頭に付加されています。
BinaryReader.ReadString メソッド ()
Writeメソッドで文字列を書き込む際、デフォルトではUTF-8にエンコードされて書き込まれますが、BinaryWriterのコンストラクタで目的のEncodingを指定することで任意のエンコーディングで文字列を書き込むことができます。
文字配列の書き込み
Writeメソッドに文字配列(char[])を指定して書き込みを行う場合は、文字列を書き込む場合とは異なり先頭に文字列の長さは書き込まれません。 そのため、ReadCharsメソッドと組み合わせて使うことで固定長の文字列フィールドの読み書きができるようになります。
以下の例では、ストリームの先頭に全フィールド数、続けて長さ8の固定長文字列フィールドを複数書き込み、それをBinaryReaderで読み込んでいます。
文字列を書き込む場合と同様、文字配列を書き込む際にはBinaryWriterのコンストラクタで指定したエンコーディングでエンコードされます。 デフォルトではUTF-8でエンコードされた上で書き込まれます。
シーク
BinaryWriterの書き込み位置を変更するにはSeekメソッドを使うことができます。 また、BaseStreamプロパティを参照してベースとなっているStreamを取得し、そして取得したStreamのSeekメソッドを呼び出すことでもシークを行うことができます。
BinaryWriterは書き込み内容のバッファリングは行われないため、BinaryReaderのシークの場合とは異なりシークを行なっても書き込まれる内容に不整合が起こることはありません。
構造体・クラスの読み書き
BinaryReader・BinaryWriterには任意の構造体・クラスを読み書きするメソッドは用意されていません。 そのため、BinaryReader・BinaryWriterで構造体やクラスを扱う場合は、以下のようにフィールドをひとつずつ読み書きする必要があります。
ストリーム中のブロックを構造体として読み書きしたい場合、fread
/fwrite
関数のような読み書きを行いたい場合は、いったんバイト配列として読み書きし、さらにバイト配列から構造体に変換する必要があります。 具体的な実装例についてはBinaryReader・BinaryWriterでの構造体の読み書きで紹介しています。
ベースとなるストリームのクローズ
BinaryReader・BinaryWriterでは、Closeメソッドを呼び出したりusingステートメントから抜けた際にはベースとなったストリームも閉じられ、ストリームに対するアクセスができなくなります。 BinaryReader・BinaryWriterを閉じたあとにベースとなったストリームに対して操作を行おうとすると例外ObjectDisposedExceptionがスローされます。 これは、StreamReader・StreamWriterでも同様です。 BinaryReader・BinaryWriterを使用したあとも引き続きストリームを使う方法についてはStreamReader・StreamWriterの解説を参照してください。
.NET Framework 4.5からは、コンストラクタの引数leaveOpenにtrueを指定すると、BinaryReader・BinaryWriterを閉じてもベースとなるストリームを開いたままにすることができます。 ただし、同時に引数encodingも省略せずに指定する必要があります。