BinaryReaderクラスおよびBinaryWriterクラスはStreamに対してバイナリデータの読み書きを行うためのクラスです。 Stream単体ではバイト配列での読み書きしか行えませんが、StreamとBinaryReader・BinaryWriterを組み合わせて使うことで構造化されたバイナリデータの読み書きが可能になります。

BinaryReader・BinaryWriterを使ったバイナリファイルの読み書き
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.datを書き込み用に開く
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // streamに書き込むためのBinaryWriterを作成
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // intの数値を書き込む
        writer.Write((int)42);

        // 4バイトのバイト配列を書き込む
        writer.Write(new byte[] {0x01, 0x23, 0x45, 0x67});
      }
    }

    // ファイルsample.datを読み込み用に開く
    using (Stream stream = File.OpenRead("sample.dat")) {
      // streamから読み込むためのBinaryReaderを作成
      using (BinaryReader reader = new BinaryReader(stream)) {
        // intの数値を読み込む
        Console.WriteLine(reader.ReadInt32());

        // 4バイト読み込む
        byte[] data = reader.ReadBytes(4);

        Console.WriteLine(BitConverter.ToString(data));
      }
    }
  }
}

BinaryReader・BinaryWriterでは、読み書きの際のバイトオーダにリトルエンディアンを使用します。 実行環境のバイトオーダによらず、常にリトルエンディアンでの読み書きが行われます。

なお、.NET Frameworkにはビッグエンディアンで読み書きを行うBinaryReader・BinaryWriterは用意されていません。 BinaryReader・BinaryWriterが使用するバイトオーダを指定したり変更することもできません。 ビッグエンディアンでの読み書きを行うには、バイトオーダの変換などを自前で実装する必要があります。 この点については、ランタイム・システム・プラットフォームの情報 §.エンディアン基本型の型変換 §.基本型とバイト配列への/からの変換などを参照してください。

§1 BinaryReader

§1.1 基本型の読み込み (ReadInt32, etc)

BinaryReaderには、基本型の読み込みを行うメソッドがいくつか用意されています。

ReadInt32等を使ってストリームから基本型の値を読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // 整数型の値の読み込み
        int i = reader.ReadInt32();

        uint ui = reader.ReadUInt32();

        byte by = reader.ReadByte();

        // 実数型の値の読み込み
        float f = reader.ReadSingle();

        double d = reader.ReadDouble();

        // bool型の値の読み込み
        bool b = reader.ReadBoolean();
      }
    }
  }
}

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がスローされます。 この際、読み込みに失敗してもストリームの現在位置は読み込む前の位置には戻りません。

BinaryReaderでの読み込みによってストリームの末尾を超える場合
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 全6バイトのデータ
    byte[] data = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xab};

    using (Stream stream = new MemoryStream(data)) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // int(4バイト)を読み込む
        int i = reader.ReadInt32();

        // ストリームの現在位置を表示
        Console.WriteLine(reader.BaseStream.Position);

        try {
          // さらにもう一度int(4バイト)を読み込もうとする
          // (実際に4バイト読み込むとストリームの末尾を超えてしまうため、例外EndOfStreamExceptionがスローされる)
          i = reader.ReadInt32();
        }
        catch (EndOfStreamException) {
          Console.WriteLine("end of stream");
        }

        // ストリームの現在位置を表示
        Console.WriteLine(reader.BaseStream.Position);
      }
    }
  }
}
実行結果
4
end of stream
6

§1.2 バイト配列の読み込み (ReadBytes, Read)

ストリームから複数バイトを読み込みバイト配列として取得するにはReadBytesメソッドを使うことができます。 このメソッドでは、読み込みたいバイト数を引数に指定します。

ReadBytesメソッドを使って指定したバイト数分をストリームから読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // ストリームから16バイト読み込みバイト配列として取得する
        byte[] data = reader.ReadBytes(16);

        // 読み込んだバイト配列の内容を表示する
        Console.WriteLine(BitConverter.ToString(data));
      }
    }
  }
}

上記の例で使用しているBitConverter.ToStringメソッドはバイト配列を見やすい形式に変換するためのもので、ReadBytesメソッドを使った読み込み処理の本質とは無関係のものです。

ReadBytesメソッドでは、ストリームの残りバイト数よりも多いバイト数を読み込もうとした場合には実際に読み込めた分のみが返されます。 読み込み中にストリームの末尾に達した場合でもEndOfStreamExceptionはスローされません。 実際に読み込めたバイト数は返されるバイト配列の長さを調べることで知ることができます。 例えば、16バイト読み込もうとして実際には残り8バイトだった場合には、長さ8のバイト配列が返されます。 ストリームの末尾に達してそれ以上読み込めるデータがない場合、ReadBytesメソッドは長さ0のバイト配列を返します。

もうひとつ、バイト配列に読み込むメソッドとしてReadメソッドも用意されています。 こちらはReadBytesメソッドとは異なり、あらかじめ用意したバイト配列を指定することにより、読み込んだデータをその配列へ格納します。 Readメソッドは、Stream.Readメソッドと同様に指定したバイト数の分だけ読み込みを試みますが、指定したバイト数ちょうどのデータが一度に読み込まれるとは限りません。 読み込み中にストリームの末尾に達した場合でもEndOfStreamExceptionはスローされません。 実際に読み込めたバイト数は戻り値で知ることができます。 ストリームの末尾に達していてそれ以上読み込めるデータがない場合、Readメソッドは0を返します。

Readメソッドを使ってストリームから指定したバイト配列にデータを読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 8バイトのデータ
    byte[] data = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};

    using (Stream stream = new MemoryStream(data)) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // 読み込んだデータを格納するためのバッファ
        byte[] buffer = new byte[16];

        // ストリームから最大6バイトを読み込み、buffer[0]以降に格納する
        int len = reader.Read(buffer, 0, 6);

        // 実際に読み込めた分を表示する
        Console.WriteLine("{0} {1}", len, BitConverter.ToString(buffer, 0, len));

        // ストリームから最大6バイトを読み込み、buffer[0]以降に格納する
        len = reader.Read(buffer, 0, 6);

        // 実際に読み込めた分を表示する
        Console.WriteLine("{0} {1}", len, BitConverter.ToString(buffer, 0, len));
      }
    }
  }
}
実行結果
6 01-23-45-67-89-AB
2 CD-EF

BinaryReaderにはStreamReader.ReadToEndメソッドのようなストリームの末尾までをひとつのバイト配列に読み込むメソッドは用意されていないため、必要な場合は自前で実装する必要があります。 次の例は、そのようなメソッドを実装した例です。 効率的な実装ではないため、あくまで参考程度のものです。

BinaryReaderを使ってストリームの末尾までをひとつのバイト配列に読み込むメソッドの実装例
using System;
using System.Collections.Generic;
using System.IO;

class Sample {
  // ストリームの末尾までを読み込みバイト配列として返すメソッド
  private static byte[] ReadToEnd(BinaryReader reader)
  {
    // ストリームの長さの取得を試みる
    long length;

    try {
      length = reader.BaseStream.Length;
    }
    catch (NotSupportedException) {
      // 長さの取得をサポートしていないストリームの場合
      length = -1;
    }

    if (length == -1) {
      // 長さを取得できない場合は、Readメソッドを使ってストリームの末尾まで読み込む

      // 一度のReadで読み込めたバイト配列を格納するリスト
      List<ArraySegment<byte>> buffers = new List<ArraySegment<byte>>();

      length = 0;

      for (;;) {
        byte[] buffer = new byte[1024];
        int len = reader.Read(buffer, 0, buffer.Length);

        if (len == 0)
          // ストリームの末尾に達した
          break;

        // 読み込めたデータを含むバイト配列をArraySegmentに格納してリストに追加
        buffers.Add(new ArraySegment<byte>(buffer, 0, len));

        length += len;
      }

      // すべてのArraySegmentを連結し、ひとつのバイト配列にして返す
      byte[] bytes = new byte[length];
      int index = 0;

      foreach (ArraySegment<byte> buffer in buffers) {
        Buffer.BlockCopy(buffer.Array, buffer.Offset, bytes, index, buffer.Count);

        index += buffer.Count;
      }

      return bytes;
    }
    else {
      // 長さを取得できた場合は、ReadBytesメソッドを使ってストリームの末尾まで読み込む
      return reader.ReadBytes((int)(length - reader.BaseStream.Position));
    }
  }

  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        byte[] bytes = ReadToEnd(reader);

        Console.WriteLine(BitConverter.ToString(bytes));
      }
    }
  }
}

上記のサンプル中で使用しているArraySegmentについては部分配列 §.ArraySegment構造体、Buffer.BlockCopyメソッドについてはバイト列操作 §.BlockCopyメソッドを参照してください。

なお、読み込み元がファイルに限定される場合はFileクラスのメソッドを使うこともできます。 File.ReadAllBytesメソッドを使うと、BinaryReaderを使わずにファイルの内容をバイト配列として読み込むことができます。

File.ReadAllBytesメソッドを使ってファイルの内容をバイト配列に読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.datを読み込み、バイト配列として取得する
    byte[] bytes = File.ReadAllBytes("sample.dat");

    Console.WriteLine(BitConverter.ToString(bytes));
  }
}

§1.3 文字列の読み込み (ReadString)

ストリームから文字列を読み込むにはReadStringメソッドを使うことができます。

ReadStringメソッドを使ってストリームから文字列を読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // ストリームから文字列を読み込み表示する
        string s = reader.ReadString();

        Console.WriteLine(s);
      }
    }
  }
}

ReadStringメソッドは、BinaryWriter.Writeメソッドで書き込まれる形式での読み込みを行います。 BinaryWrite.Writeメソッドで文字列を書き込む場合、先頭に文字列の長さが書き込まれます。 ReadStringメソッドはその長さを元に文字列の読み込みを行います。

従ってReadStringメソッドでは、読み込む文字列の長さを事前に知っている必要はなく、引数で読み込む文字列の長さを指定する必要もありません。 逆に、ReadStringメソッドを固定長の文字列フィールドを読み込む目的に使用することはできません。 固定長の文字列フィールドの読み込みを行う場合は、ReadBytesメソッドReadCharsメソッドを使って読み込み処理を記述する必要があります。 固定長の文字列フィールドを読み書きする具体例はBinaryWriter.Writeメソッドの解説をご覧ください。

ReadStringメソッドで文字列を読み込む際、デフォルトではUTF-8でデコードして読み込みますが、BinaryReaderのコンストラクタで目的のEncodingを指定することで任意のエンコーディングで文字列を読み込むことができます。 当然、BinaryWriter.Writeメソッドで書き込んだときと同じエンコーディングを指定しないと正しくデコードされません。

エンコーディングを指定した上でReadStringメソッドを使ってストリームから文字列を読み込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      // 文字列をデコードする際にShift_JISを使用するように指定してBinaryReaderを作成
      using (BinaryReader reader = new BinaryReader(stream, Encoding.GetEncoding("Shift_JIS"))) {
        // ストリームから文字列を読み込み表示する
        string s = reader.ReadString();

        Console.WriteLine(s);
      }
    }
  }
}

§1.4 文字配列の読み込み (ReadChars)

ストリームから複数バイトを読み込み文字配列(char[])として取得するにはReadCharsメソッドを使うことができます。 このメソッドはReadBytesメソッドと似ていますが、引数にはバイト数ではなく読み込む文字数を指定します。 したがって、実際に読み込まれるバイト数はBinaryReaderが使用するエンコーディングによって変わります。

ReadCharsメソッドを使ってストリームから指定した文字数を読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // ストリームから4文字読み込みchar配列として取得する
        char[] chars = reader.ReadChars(4);

        // 読み込んだchar配列を文字列に変換して表示
        string s = new string(chars);

        Console.WriteLine(s);
      }
    }
  }
}

ReadCharsメソッドを使うことでストリームから固定長の文字列フィールドを読み込むことができます。 固定長の文字列フィールドを読み書きする具体例はBinaryWriter.Writeメソッドの解説をご覧ください。

ReadStringメソッドの場合と同様、ReadCharメソッドはBinaryReaderのコンストラクタで指定したエンコーディングでのデコードを行います。 デフォルトではUTF-8でエンコードされているものとして読み込まれます。



§1.5 文字単位での読み込み・先読み (Read, PeekChar)

Readメソッドを引数なしで呼び出すと、ストリームから1文字(char)を読み込みます。 ReadCharメソッドと似ていますが、戻り値はint/Integerで、ストリームの末尾に達した場合は-1が返されます。 そのため、ストリームの末尾に達した状態でこのメソッドを呼び出しても例外EndOfStreamExceptionはスローされません。

Readメソッドを使ってストリームから一文字ずつ読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // 無限ループ
        for (;;) {
          // 一文字読み込む
          int ch = reader.Read();

          if (ch == -1)
            // ストリームの末尾(EOF)に達したので読み込みを終了する
            break;

          // 読み込んだ値をcharにキャストして表示する
          char c = (char)ch;

         Console.Write(c);
        }
      }
    }
  }
}

ReadStringメソッドの場合と同様、ReadメソッドはBinaryReaderのコンストラクタで指定したエンコーディングでのデコードを行います。 デフォルトではUTF-8でエンコードされているものとして読み込まれます。

PeekCharメソッドを使うと、次の1文字を先読みすることができます。 Peekメソッドの戻り値は引数なしのReadメソッドと同様です。

§1.6 シーク

BinaryReaderにはストリームのシークを行うメソッドは用意されていません。 シークを行うには、まずBaseStreamプロパティを参照してベースとなっているStreamを取得し、そして取得したStreamのSeekメソッドを呼び出すようにします。

BaseStreamを参照してBinaryReaderをシークする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // データブロックのサイズを読み込む
        int blockSize = reader.ReadInt32();

        // シークしてデータブロックを読み飛ばす
        reader.BaseStream.Seek(blockSize, SeekOrigin.Current);

        // 次のデータを読み込む
        int data = reader.ReadInt32();
      }
    }
  }
}

BinaryReaderでは内部でデータの先読みとバッファリングが行われるようになっているため、BaseStreamを参照してストリームのシークを行うと読み込まれる内容に不整合が起こる可能性が考えられます。 実際にそういったことが起こるかどうかはドキュメントには明記されていないため不明確ですが、BinaryReaderにSeekメソッドが用意されていない点を勘案すると、BinaryReaderでランダムアクセスを行うのは避けたほうがよいと思われます。

データを読み飛ばす目的でシークを行いたい場合には、シークを行うかわりにReadBytes等のメソッドを使ってシークしたい分だけデータを読み込み、その戻り値は単に破棄する、といった方法をとることができます。 この方法の場合、ストリーム後方へのシークのみしかできないものの、操作によってバッファリングされている内容に不整合が起きることはないため、確実な方法と言えます。

ReadBytesメソッドでデータを読み捨てることでストリーム後方へのシークを行う
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // データブロックのサイズを読み込む
        int blockSize = reader.ReadInt32();

        // データブロックを読み捨てる
        reader.ReadBytes(blockSize);

        // 次のデータを読み込む
        int data = reader.ReadInt32();
      }
    }
  }
}

§2 BinaryWriter

§2.1 基本型の書き込み

BinaryWriterで書き込みを行う場合はWriteメソッドを使います。 このメソッドでは与えられた引数の型に従って内容が書き込まれます。 Writeメソッドで基本型の書き込みを行う際、数値リテラルを直接指定して書き込む場合は、リテラルにサフィックスをつけたり明示的にキャストすることにより、書き込もうとしている値の型を明確にすることをおすすめします。

Writeメソッドで基本型の値をストリームに書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // 整数型の値の書き込み
        writer.Write((int)16);

        writer.Write((uint)42);

        writer.Write((byte)72);

        // 実数型の値の読み込み
        writer.Write(0.05f); // float

        writer.Write(Math.PI); // double

        // bool型の値の読み込み
        writer.Write(true);
      }
    }
  }
}

Writeメソッドで書き込める基本型の種類はReadメソッドと同じです。 WriteメソッドではDateTimeやDateTimeOffsetを直接書き込むことはできないので、ToBinary/FromBinaryなどのメソッドを使って数値などに変換してから書き込む必要があります。

§2.2 バイト配列の書き込み

Writeメソッドではバイト配列の書き込みにも対応しています。 引数に指定する値と意味はStream.Writeメソッドと同じです。 BinaryWriter.Writeメソッドでは、バイト配列のみを指定することでその内容すべてを書き込むようにすることもできます。

Writeメソッドを使ってバイト配列の内容をストリームに書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // 書き込むデータが格納されているバイト配列
        byte[] data = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

        // dataの0番目から4バイト分(buffer[0]〜buffer[3])を書き込む
        writer.Write(data, 0, 4);

        // dataの4番目から4バイト分(buffer[4]〜buffer[7])を書き込む
        writer.Write(data, 4, 4);

        // data[0]〜data[7]のすべてを書き込む
        writer.Write(data);
      }
    }
  }
}

なお、書き込み先がファイルに限定される場合はFileクラスのメソッドを使うこともできます。 File.WriteAllBytesメソッドを使うと、BinaryWriterを使わずにバイト配列をファイルに書き込むことができます。

File.WriteAllBytesメソッドを使ってバイト配列の内容をファイルとして書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 書き込むデータが格納されているバイト配列
    byte[] data = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

    // バイト配列の内容をファイルsample.datに書き込む
    File.WriteAllBytes("sample.dat", data);
  }
}

§2.3 文字列の書き込み

Writeメソッドは文字列の書き込みにも対応しています。

Writeメソッドを使ってストリームに文字列を書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (MemoryStream stream = new MemoryStream()) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // Writeメソッドで文字列を書き込む
        writer.Write("ABCDEFGH");
      }

      // 実際に書き込まれた内容を表示する
      Console.WriteLine(BitConverter.ToString(stream.ToArray()));
    }
  }
}
実行結果
08-41-42-43-44-45-46-47-48

BinaryWriterで文字列を書き込む場合、まず文字列の長さがストリームに書き込まれ、続けて文字列のバイト表現が書き込まれます。 これにより、読み込みの際に文字列の具体的な長さを知らなくても任意の長さの文字列を読み込むことができるようになっています。 実際、文字列を読み込むBinaryReader.ReadStringメソッドには引数で読み込む文字列の長さを指定することはできません。

上記の実行結果における1バイト目の 0x08 は後続する文字列のバイト数が 8 であることを表しています。 BinaryReader.ReadStringメソッドのドキュメントによると、文字列とその長さは次のように書き込まれます。

現在のストリームから 1 つの文字列を読み取ります。ストリームの文字列は、7 ビットごとにエンコードされた文字列の長さが先頭に付加されています。

BinaryReader.ReadString メソッド ()

Writeメソッドで文字列を書き込む際、デフォルトではUTF-8にエンコードされて書き込まれますが、BinaryWriterのコンストラクタで目的のEncodingを指定することで任意のエンコーディングで文字列を書き込むことができます。

エンコーディングを指定した上でWriteメソッドを使ってストリームに文字列を書き込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 文字列をエンコードする際にShift_JISを使用するように指定してBinaryWriterを作成
      using (BinaryWriter writer = new BinaryWriter(stream, Encoding.GetEncoding("Shift_JIS"))) {
        // Writeメソッドで文字列を書き込む
        writer.Write("あいう日本語");
      }
    }
  }
}

§2.4 文字配列の書き込み

Writeメソッドに文字配列(char[])を指定して書き込みを行う場合は、文字列を書き込む場合とは異なり先頭に文字列の長さは書き込まれません。 そのため、ReadCharsメソッドと組み合わせて使うことで固定長の文字列フィールドの読み書きができるようになります。

以下の例では、ストリームの先頭に全フィールド数、続けて長さ8の固定長文字列フィールドを複数書き込み、それをBinaryReaderで読み込んでいます。

Writeメソッド・ReadCharsメソッドを使って固定長の文字列フィールドを読み書きする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルに固定長文字列データを複数書き込む
    using (Stream stream = File.OpenWrite("sample.dat")) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // 書き込む文字列
        string[] fields = new string[] {
          "0123456789",
          "あいう日本語",
          "Hello, world!",
        };

        // 全フィールド数を書き込む
        writer.Write(fields.Length);

        // 長さ8の固定長文字列フィールドを書き込む
        foreach (string field in fields) {
          // 書き込む文字列の長さがちょうど8となるように加工する
          string s = field;

          if (8 < field.Length)
            // 8より長い場合は、8文字に切り詰める
            s = field.Substring(0, 8);
          else if (field.Length < 8)
            // 8より短い場合は、8文字になるまでヌル文字で埋める
            s = field.PadRight(8, '\0');

          // 文字列をchar[]に変換して書き込む
          writer.Write(s.ToCharArray());
        }
      }
    }

    // ファイルから固定長文字列データを複数読み込む
    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // 全フィールド数を読み込む
        int fieldCount = reader.ReadInt32();

        for (int i = 0; i < fieldCount; i++) {
          // 長さ8の固定長文字列フィールドを読み込み、文字列に変換する
          string s = new string(reader.ReadChars(8));

          // ヌル文字で埋められている部分を削る
          s = s.TrimEnd('\0');

          // 得られた文字列を表示
          Console.WriteLine("'{0}'", s);
        }
      }
    }
  }
}
実行結果
'01234567'
'あいう日本語'
'Hello, w'

文字列を書き込む場合と同様、文字配列を書き込む際にはBinaryWriterのコンストラクタで指定したエンコーディングでエンコードされます。 デフォルトではUTF-8でエンコードされた上で書き込まれます。

§2.5 シーク

BinaryWriterの書き込み位置を変更するにはSeekメソッドを使うことができます。 また、BaseStreamプロパティを参照してベースとなっているStreamを取得し、そして取得したStreamのSeekメソッドを呼び出すことでもシークを行うことができます。

Seekメソッドを使ってストリームをシークする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // データを書き込む
        writer.Write((int)16);

        // ストリームの先頭にシーク
        writer.Seek(0, SeekOrigin.Begin);
        //writer.BaseStream.Seek(0, SeekOrigin.Begin); // このようにしても同じ

        // 別のデータを上書きする
        writer.Write((int)42);
      }
    }
  }
}

BinaryWriterは書き込み内容のバッファリングは行われないため、BinaryReaderのシークの場合とは異なりシークを行なっても書き込まれる内容に不整合が起こることはありません。

§3 構造体・クラスの読み書き

BinaryReader・BinaryWriterには任意の構造体・クラスを読み書きするメソッドは用意されていません。 そのため、BinaryReader・BinaryWriterで構造体やクラスを扱う場合は、以下のようにフィールドをひとつずつ読み書きする必要があります。

BinaryReader/BinaryWriterを用いて構造体の読み書きを行う
using System;
using System.IO;

// ファイルへの読み書きで使用する構造体
struct Date {
  public short Year;
  public byte Month;
  public byte Day;
}

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      using (BinaryWriter writer = new BinaryWriter(stream)) {
        // 構造体を作成し、値を設定
        Date d = new Date();

        d.Year = 2013;
        d.Month = 4;
        d.Day = 1;

        // 構造体のフィールドを1つずつ書き込む
        writer.Write(d.Year);
        writer.Write(d.Month);
        writer.Write(d.Day);
      }
    }

    using (Stream stream = File.OpenRead("sample.dat")) {
      using (BinaryReader reader = new BinaryReader(stream)) {
        // 構造体を作成し、値を読み込む
        Date d = new Date();

        d.Year = reader.ReadInt16();
        d.Month = reader.ReadByte();
        d.Day = reader.ReadByte();

        // 構造体に読み込んだ内容を表示
        Console.WriteLine("{0}-{1}-{2}", d.Year, d.Month, d.Day);
      }
    }
  }
}

ストリーム中のブロックを構造体として読み書きしたい場合、fread/fwrite関数のような読み書きを行いたい場合は、いったんバイト配列として読み書きし、さらにバイト配列から構造体に変換する必要があります。 具体的な実装例についてはBinaryReader・BinaryWriterでの構造体の読み書きで紹介しています。

§4 ベースとなるストリームのクローズ

BinaryReader・BinaryWriterでは、Closeメソッドを呼び出したりusingステートメントから抜けた際にはベースとなったストリームも閉じられ、ストリームに対するアクセスができなくなります。 BinaryReader・BinaryWriterを閉じたあとにベースとなったストリームに対して操作を行おうとすると例外ObjectDisposedExceptionがスローされます。 これは、StreamReader・StreamWriterでも同様です。 BinaryReader・BinaryWriterを使用したあとも引き続きストリームを使う方法についてはStreamReader・StreamWriterの解説を参照してください。

.NET Framework 4.5からは、コンストラクタの引数leaveOpenにtrueを指定すると、BinaryReader・BinaryWriterを閉じてもベースとなるストリームを開いたままにすることができます。 ただし、同時に引数encodingも省略せずに指定する必要があります。

コンストラクタの引数leaveOpenを指定してBinaryReader・BinaryWriterを閉じてもベースとなるストリームを開いたままにする
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      // leaveOpenにtrueを指定してBinaryReaderを作成
      const bool leaveOpen = true;

      using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen)) {
        reader.ReadByte();
      }

      // Usingステートメントから抜けてreaderが閉じられても
      // streamは閉じられないのでObjectDisposedExceptionはスローされない
      stream.ReadByte();
    }

    using (Stream stream = File.OpenWrite("sample.dat")) {
      // leaveOpenにtrueを指定してBinaryWriterを作成
      const bool leaveOpen = true;

      using (BinaryWriter writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen)) {
        writer.Write(16);
      }

      // BinaryWriterの場合も同様で、ObjectDisposedExceptionはスローされない
      stream.WriteByte(0);
    }
  }
}