BinaryReader/Writerクラス

ストリームでバイナリを手軽に扱うクラス

Streamクラスを利用したデータの読み書きは、データをbyte型やbyte型の配列で扱います。
そのままではやや扱いづらいので、BinaryReaderBinaryWriterクラスを利用した読み書き方法が提供されています。

BinaryReader/WriterクラスはStream派生クラスと組み合わせて使用します。
まずStream派生クラスのインスタンスを生成し、それをBinaryReader/Writerのコンストラクターに渡します。
これでBinaryReader/WriterはStreamとの間に入ってデータを適切に加工してくれます。


MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms);

//bwを通してmsに書き込み
bw.Write(123);

bw.Close();
ms.Close(); //必要ない

BinaryReader/Writerクラスを閉じたとき、操作対象のストリームも同時に閉じるのでClose(Dispose)メソッドを二回呼ぶ必要はありません。
BinaryReader/Writerを閉じても元のストリームを閉じないように動作を変更することも可能です。

このページではusingステートメントを利用したコードで記述します。


using (MemoryStream ms = new MemoryStream())
using (BinaryReader br = new BinaryReader(ms))
{
    //読み書き処理...
}
//この時点でmsとbrはクローズ、破棄されている

共通の操作

コンストラクター

BinaryReader/Writerクラスのコンストラクターは、他の(Stream派生クラスの)ストリームのインスタンスを指定します。
他にふたつのオーバーロードがあります。
以下はBinaryReaderクラスを例に説明しますが、BinaryWriterクラスも同様のオーバーロードがあります。

オーバーロード 説明
BinaryReader(Stream input) 指定のストリームをBinaryReaderで読み取る。
エンコーディングはUTF-8を使用する。
BinaryReader(Stream input, Encoding encoding) 指定のストリームをBinaryReaderで読み取る。
文字エンコーディングはencodingを使用する。
BinaryReader(Stream input, Encoding encoding, bool leaveOpen) 指定のストリームをBinaryReaderで読み取る。
エンコーディングはencodingを使用する。
leaveOpenにtrueを指定すると、BinaryReaderのインスタンスのクローズ後に元のストリームを開いたままにする。
(.NET Framework4.5以降)

文字エンコーディング

BinaryReader/Writerクラスは、文字や文字列の読み書きの際に標準でUTF-8を使用します。
文字コードを変更する場合はコンストラクターでEncodingクラスを指定します。
(System.Text名前空間)


//UTF-16
using (var ms = new MemoryStream())
using (var bw = new BinaryWriter(ms, Encoding.Unicode))
{
}

//Shift_JIS
using (var ms = new MemoryStream())
using (var bw = new BinaryWriter(ms, Encoding.GetEncoding("Shift_JIS")))
{
}

詳しくはEncodingクラスを参照してください。

BaseStreamプロパティ

BinaryReader/WriterのインスタンスはBaseStreamプロパティで操作対象のストリームを取得することができます。


using (MemoryStream ms = new MemoryStream())
using (BinaryReader br = new BinaryReader(ms))
{
    //BaseStreamプロパティはMemoryStreamを返すので
    //MemoryStreamのメソッドを使用可能
    br.BaseStream.WriteByte((byte)1);
}

ただし、同じ操作がBinaryReader/Writerクラスで提供されている場合、そちらを通して操作を行うことが推奨されます。
BaseStreamプロパティからベースとなるストリームの操作を行った場合、BinaryReader/Writer側の操作は行われないので、必要な処理が行われない可能性があります。
ベースとなるストリームのインスタンス(上記コードの例では変数ms)を直接操作することも同様なので注意してください。

シーク

シーク操作はBinaryWriterクラスにはSeekメソッドがあるので、これを使用します。
BinaryReaderクラスにはSeekメソッドがないので、BaseStreamプロパティを経由してSeekメソッドを実行するか、Positionプロパティを操作します。


using (MemoryStream ms = new MemoryStream())
using (BinaryWriter bw = new BinaryWriter(ms))
{
    for (int i = 0; i < 255; i++)
        bw.Write((byte)i);

    bw.Seek(0, SeekOrigin.Begin);

    bw.BaseStream.Seek(0, SeekOrigin.End);
    bw.BaseStream.Position = 0;
}

BinaryWriterクラス

Writeメソッド

データの書き込みにはBinaryWriterクラスのWriteメソッドを使用します。


string path = "test.bin";

byte[] bytes = new byte[] { 1, 2, 3, 4, 5 };
char[] chars = new char[] { 'a', 'b', 'c', 'd', 'e' };

using (FileStream fs = new FileStream(path, FileMode.Create))
using (BinaryWriter bw = new BinaryWriter(fs))
{
    bw.Write(123);  //int型
    bw.Write(4.56); //double型
    bw.Write(true); //bool型
    bw.Write("あいうえお"); //string型

    //byte型配列をすべて書き込み
    bw.Write(bytes);

    //配列の2番目の要素から3つ分を書き込み
    bw.Write(bytes, 2, 3);

    //char型配列をすべて書き込み
    bw.Write(chars);

    //配列の2番目の要素から3つ分を書き込み
    bw.Write(chars, 2, 3);
}

Writeメソッドはobject型以外の組み込み型をそのまま書き込むことができます。
その他、byte型配列とchar型配列を書き込むこともできます。
戻り値はありません。

Write7BitEncodedIntメソッド

Write7BitEncodedIntメソッドは、int型(32bit整数)の数値を圧縮形式で書き込みます。
Write7BitEncodedInt64メソッドは、long型(64bit整数)の数値を圧縮形式で書き込みます。


using (MemoryStream ms = new MemoryStream())
using (BinaryWriter bw = new BinaryWriter(ms))
{
    bw.Write7BitEncodedInt(10);
    bw.Write7BitEncodedInt(100);
    bw.Write7BitEncodedInt(1000);

    bw.Flush();
    bw.Seek(0, SeekOrigin.Begin);

    int b = 0;
    while ((b = ms.ReadByte()) >= 0) {
        Console.Write("{0} ", b);
    }
}
10 100 232 7

int型は通常であればその値に関係なく4バイト必要ですが、このメソッドは値が小さい場合にはサイズを縮小して書き込める可能性があります。
逆に、大きな値を書き込んだ場合は5バイトを必要とする場合もあります。
また、負数は常に最大のバイトを必要とするようです。
(Write7BitEncodedIntなら5バイト、Write7BitEncodedInt64なら9バイト)

このメソッドで書き込んだ数値は、必ずBinaryReaderクラスのRead7BitEncodedInt(Read7BitEncodedInt64)メソッドで読み取る必要があります。

ちなみに値とバイト数の関係は以下のようになります。
(「2^7」は2の7乗の意味)

バイト数
-1以下 5(32bit整数)
9(64bit整数)
0~127(2^7 - 1) 1
128~16,383(2^14 - 1) 2
16,384~2,097,151(2^21 - 1) 3
2,097,152~268,435,455(2^28 - 1) 4
268,435,456~34,359,738,367(2^35 - 1)
(int.MaxValue=2,147,483,647)
5
34,359,738,368~
4,398,046,511,103(2^42 - 1)
6
4,398,046,511,104~
562,949,953,421,311(2^49 - 1)
7
562,949,953,421,312~
72,057,594,037,927,935(2^56 - 1)
8
72,057,594,037,927,936~
9,223,372,036,854,775,807(long.MaxValue)
9

メソッド名にもある通り、このメソッドはデータを7bit単位で区切って表現します。
残りの1bitは「全てのデータが変換されたか否か」を表すフラグに使用します。
7bitに収まる範囲の値は7bitデータに変換し、フラグには「0」がセットされるので1バイトのサイズで表現できます。
7bitに収まらない場合はフラグには「1」をセットし、次の7bitが使用されます。
これを全てのデータが格納されるまで繰り返します。

Flushメソッド

FlushメソッドはBinaryWriter内のバッファを書き出してストリームに反映させます。


using (FileStream fs = new FileStream("test.bin", FileMode.Create))
using (BinaryWriter bw = new BinaryWriter(fs))
{
    bw.Write(123);
    bw.Write("abc");

    //FileStreamにデータを反映
    bw.Flush();
}
//BinaryWriterをクローズすることでも
//元ストリームに反映される

FlushメソッドはベースとなるストリームのFlushメソッドも呼び出すので、上記コードはファイルへの書き込みが行われます。

FlushメソッドはStreamクラスで抽象メソッドとして定義されているので、すべてのStream派生クラスで使用することができます。

BinaryReaderクラス

Read○○系メソッド

データの読み込みにはBinaryReaderクラスのRead○○系のメソッドを使用します。
ここでは先ほどのBinaryWriterクラスのWriteメソッドの説明で作成したファイル「test.bin」を読み込んでみます。


string path = "test.bin";

using (FileStream fs = new FileStream(path, FileMode.Open))
using (BinaryReader br = new BinaryReader(fs))
{
    Console.WriteLine(br.ReadInt32());
    Console.WriteLine(br.ReadDouble());
    Console.WriteLine(br.ReadBoolean());
    Console.WriteLine(br.ReadString());

    foreach (var b in br.ReadBytes(5))
        Console.Write("{0} ", b);
    Console.WriteLine();

    foreach (var b in br.ReadBytes(3))
        Console.Write("{0} ", b);
    Console.WriteLine();

    foreach (var c in br.ReadChars(5))
        Console.Write("{0} ", c);
    Console.WriteLine();

    foreach (var c in br.ReadChars(3))
        Console.Write("{0} ", c);
    Console.WriteLine();
}
123
4.56
True
あいうえお
1 2 3 4 5 
3 4 5 
a b c d e 
c d e 

BinaryReaderの読み取りメソッドには以下があります。

メソッド名 読み取るサイズ 戻り値の型
ReadByte 1バイト byte型
ReadSByte 1バイト sbyte型
ReadInt16 2バイト short型
ReadUInt16 2バイト ushort型
ReadInt32 4バイト int型
ReadUInt32 4バイト uint型
ReadInt64 8バイト long型
ReadUInt64 8バイト ulong型
ReadSingle 4バイト float型
ReadDouble 8バイト double型
ReadDecimal 16バイト decimal型
ReadBoolean 4バイト bool型
ReadChar 1文字 char型
ReadString 任意 string型

これらのメソッドはストリームの現在の位置から指定のバイト数分を読み取り、指定のデータ型に変換して返します。
現在の位置に期待通りのデータが存在しなければ正しいデータは得られません。

ReadCharメソッドは「1バイト」ではなく「1文字」を読み取ります。
1文字のバイト数は文字コードによって変わります。

ReadStringメソッドは文字列を読み取りますが、読み取れるのはBinaryWriterを利用して書き込んだ文字列に限ります。
BinaryWriterの文字列書き込みはデータの先頭に文字列の長さ情報を格納しており、BinaryReaderはこの情報を利用して読み取る文字列の長さを決定します。
BinaryWriterを経由せずに文字列を書き込んだ場合は長さ情報が格納されていないため、ReadStringメソッドでは読み取ることはできません。

上記の表のメソッドは、ストリームの終端を超えてデータを読み取ろうとすると例外(EndOfStreamException)が発生します。

ReadBytes、ReadCharsメソッド

その他、ReadBytesメソッドはbyte型配列を、ReadCharsメソッドはchar型配列を読み取れます。
ReadBytesメソッドの引数は読み取るバイト数を指定します。
ReadCharsメソッドの引数は読み取る文字数を指定します。

この二つのメソッドはストリームの末尾を超えてデータを読み取ろうとするとストリームの残りのデータの配列が返されます。
例えばストリームの残りが5バイトのとき、br.ReadBytes(10)を実行すると要素数5のbyte型配列が返されます。
ストリームが終端の場合は要素数0の配列が返されます。
つまり例外は発生しません。

Read、PeekCharメソッド

データ読み取りメソッドにはReadメソッドとPeekCharメソッドも使用できます。

メソッド 説明 戻り値 ストリーム終端を超えた場合
Read() 1文字を読み取る
読み取ったデータが現在の文字列エンコードに変換できない場合は例外発生(ArgumentException)
読み取った文字(int型) -1を返す
Read(byte[] buffer, int index, int count) countバイトを読み取りbuffer[index]から順に格納 読み取れたバイト数(int型) 残りのデータをbufferに格納し、読み取れたバイト数を返す
すでに終端に位置する場合は0を返す
Read(char[] buffer, int index, int count) count文字分を読み取りbuffer[index]から順に格納 読み取れた文字数(int型) 残りのデータをbufferに格納し、読み取れた文字数を返す
すでに終端に位置する場合は0を返す
PeekChar() 1文字を読み取るが、ストリームの位置は移動させない
読み取ったデータが現在の文字列エンコードに変換できない場合は例外発生(ArgumentException)
読み取った文字(int型) -1を返す

Read7BitEncodedIntメソッド

Read7BitEncodedIntメソッドは、圧縮形式で書き込まれたint型(32bit整数)を読み取ります。
Read7BitEncodedInt64メソッドは、圧縮形式で書き込まれたlong型(64bit整数)を読み取ります。

これらはWrite7BitEncodedInt(Write7BitEncodedInt64)メソッドで書き込んだデータを読み取るためのものです。
通常のint型(long型)のデータは読み取れません。

読み取られるバイト数は値によって1~5バイトの範囲で変化します。
(Read7BitEncodedInt64メソッドの場合は1~9バイト)


using (MemoryStream ms = new MemoryStream())
{
    //BinaryWriterを閉じてもMemoryStreamは開いたままにする
    using (BinaryWriter bw = new BinaryWriter(ms, Encoding.UTF8, true))
    {
        bw.Write7BitEncodedInt(10);
        bw.Write7BitEncodedInt(100);
        bw.Write7BitEncodedInt(1000);
    }
    ms.Position = 0;
    using (BinaryReader br = new BinaryReader(ms))
    {
        int a = br.Read7BitEncodedInt();
        Console.WriteLine(
            "value: {0}, Position: {1}",
            a, br.BaseStream.Position);
        int b = br.Read7BitEncodedInt();
        Console.WriteLine(
            "value: {0}, Position: {1}", 
            b, br.BaseStream.Position);
        int c = br.Read7BitEncodedInt();
        Console.WriteLine(
            "value: {0}, Position: {1}",
            c, br.BaseStream.Position);
    }
}
value: 10, Position: 1
value: 100, Position: 2
value: 1000, Position: 4

「10」と「100」は1バイト、「1000」は2バイトのサイズであることが分かります。