MemoryStreamクラスはメモリ上に確保されたバイト配列に対して読み書きを行うためのStreamです。 Streamクラスから派生したクラスであるため、ほとんどの機能と操作方法はStreamクラスと同じであることから、その部分については省略します。 MemoryStreamを使った読み込み・書き込み等の操作についてはStreamクラスStreamReaderクラス・StreamWriterクラスBinaryReaderクラス・BinaryWriterクラスを参照してください。 ここではMemoryStreamクラスに特有な箇所についてのみ解説します。

§1 インスタンスの作成

MemoryStreamでは、読み書きに使用するバッファ(バイト配列)をMemoryStream自身に作成させるか、それとも別に存在する既存のバイト配列を使用するかを指定することができます。

MemoryStream自身にバッファを作成させるようにした場合、MemoryStreamに書き込む内容のサイズに応じて自動的にバッファサイズが拡張されます(可変長のバッファ)。 この場合、SetLengthメソッドでストリームの長さ(サイズ)を変更することも可能となります。

一方、既存のバイト配列を使うように指定した場合、MemoryStreamでは指定されたバイト配列への読み書きのみを行います。 指定されたバイト配列のサイズを超えて書き込もうとした場合や、SetLengthメソッドで長さを変更しようとした場合には例外NotSupportedExceptionがスローされます。 また、既存のバイト配列を指定する場合は、バイト配列の一部分のみを参照できるようにしたり、書き込みを禁止して読み込み専用にすることもできます。

使用するバッファや書き込みの許可などの指定は、次のようにMemoryStreamのインスタンスを作成する際にコンストラクタで指定します。

MemoryStreamを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 書き込む内容に応じて自動的に拡張されるバッファを使用するMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream()) {
      // BinaryWriterを使ってデータを書き込む
      BinaryWriter writer = new BinaryWriter(stream);

      writer.Write(0x12345678);

      // MemoryStreamに書き込んだ内容をバイト配列として取得して表示
      byte[] array = stream.ToArray();

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

    // 既存のバイト配列からデータを参照する読み込み専用のMemoryStreamを作成
    byte[] data = new byte[] {0x78, 0x56, 0x34, 0x12};

    using (MemoryStream stream = new MemoryStream(data, false)) {
      // BinaryReaderを使ってデータを読み込む
      BinaryReader reader = new BinaryReader(stream);

      Console.WriteLine("{0:x8}", reader.ReadInt32());
    }
  }
}

MemoryStreamのコンストラクタにはいくつかの種類があります。 コンストラクタと作成されるMemoryStreamの動作・ケーパビリティをまとめると次のようになります。

MemoryStreamのコンストラクタとインスタンスの動作・ケーパビリティ
コンストラクタのオーバーロード コンストラクタの動作と作成されるMemoryStream 書き込み サイズの変更
new MemoryStream() 可変長のバッファを使用して読み書きを行う
new MemoryStream(capacity) 可変長のバッファを使用して読み書きを行う
バッファの初期サイズをcapacityで指定する
new MemoryStream(buffer) 既存のバイト配列bufferに対して読み書きを行う 不可
new MemoryStream(buffer, writable) 既存のバイト配列bufferに対して読み書きを行う
bufferへの書き込みを許可するかどうかをwritableで指定する
writableの指定による 不可
new MemoryStream(buffer, index, count) 既存のバイト配列bufferの[index]〜[index + count - 1]の範囲のみに対して読み書きを行う 不可
new MemoryStream(buffer, index, count, writable) 既存のバイト配列bufferの[index]〜[index + count - 1]の範囲のみに対して読み書きを行う
bufferへの書き込みを許可するかどうかをwritableで指定する
writableの指定による 不可

現在確保されているバッファのサイズを取得するにはCapacityプロパティを参照します。 この値は、現在のストリームの長さ(Length)とはことなるものです。



§2 バイト配列への変換 (ToArray)

MemoryStreamに書き込んだ内容をバイト配列に変換して取得するには、ToArrayメソッドを呼び出します。

ToArrayメソッドを使ってMemoryStreamの内容をバイト配列に変換する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (MemoryStream stream = new MemoryStream()) {
      // BinaryWriterを使ってデータを書き込む
      BinaryWriter writer = new BinaryWriter(stream);

      writer.Write((uint)0x01234567);
      writer.Write((ushort)0x89AB);
      writer.Write((byte)0xCD);
      writer.Write((byte)0xEF);

      // MemoryStreamに書き込んだ内容をバイト配列として取得して表示
      byte[] array = stream.ToArray();

      Console.WriteLine("Length = {0}", array.Length);
      Console.WriteLine(BitConverter.ToString(array));
    }
  }
}
実行結果
Length = 8
67-45-23-01-AB-89-CD-EF

ToArrayメソッドは、常にMemoryStreamの内容を新しく作成した配列にコピーして返します。 既存のバイト配列を指定してMemoryStreamを作成した場合、ToArrayメソッドではその配列とは別のインスタンスが返されます。 また、既存のバイト配列の一部を指定したMemoryStreamでは、その範囲・長さの配列が返されます。

既存の配列からMemoryStreamを作成した場合におけるToArrayメソッドが返す配列インスタンス
using System;
using System.IO;

class Sample {
  static void Main()
  {
    byte[] data = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};

    // dataを使ってMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(data)) {
      // streamをバイト配列に変換
      byte[] array = stream.ToArray();

      Console.WriteLine("data  : {0}", BitConverter.ToString(data));
      Console.WriteLine("array : {0}", BitConverter.ToString(array));
      Console.WriteLine("data == array : {0}", data == array);
      Console.WriteLine();
    }

    // dataの2バイト目から4バイト分を使ってMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(data, 2, 4)) {
      // streamをバイト配列に変換
      byte[] array = stream.ToArray();

      Console.WriteLine("data  : {0}", BitConverter.ToString(data));
      Console.WriteLine("array : {0}", BitConverter.ToString(array));
      Console.WriteLine("data == array : {0}", data == array);
      Console.WriteLine();
    }
  }
}
実行結果
data  : 01-23-45-67-89-AB-CD-EF
array : 01-23-45-67-89-AB-CD-EF
data == array : False

data  : 01-23-45-67-89-AB-CD-EF
array : 45-67-89-AB
data == array : False

§3 バッファの取得

§3.1 GetBufferメソッド

ToArrayメソッドではMemoryStreamのコピーが返されますが、MemoryStreamの内部で使用されるバッファを直接参照する必要がある場合にはGetBufferメソッドを使います。

GetBufferメソッドを使ってMemoryStreamの内部バッファを直接参照する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 初期バッファサイズに32を指定したMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(32)) {
      // BinaryWriterを使ってデータを書き込む
      BinaryWriter writer = new BinaryWriter(stream);

      writer.Write((uint)0x01234567);
      writer.Write((ushort)0x89AB);
      writer.Write((byte)0xCD);
      writer.Write((byte)0xEF);

      Console.WriteLine("stream.Length = {0}", stream.Length);
      Console.WriteLine("stream.Capacity = {0}", stream.Capacity);

      // MemoryStreamの内部バッファを取得する
      byte[] buffer = stream.GetBuffer();

      Console.WriteLine("buffer.Length = {0}", buffer.Length);
      Console.WriteLine(BitConverter.ToString(buffer));
    }
  }
}
実行結果
stream.Length = 8
stream.Capacity = 32
buffer.Length = 32
67-45-23-01-AB-89-CD-EF-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00

MemoryStreamのコンストラクタで既存のバイト配列を指定した場合は、GetBufferメソッドからはそのバイト配列(同じインスタンス)が返されます。 ただし、コンストラクタで引数publiclyVisibleにtrueを指定しないとGetBufferメソッドでバッファを取得することは出来ません。 publiclyVisibleにfalseを指定したMemoryStreamに対してGetBufferメソッドを呼び出した場合、例外UnauthorizedAccessExceptionがスローされます。

GetBufferメソッドによるMemoryStream内部バッファの取得を禁止する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    byte[] data = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};

    // publiclyVisibleにtrueを指定してMemoryStreamを作成
    bool publiclyVisible = true;

    Console.WriteLine("publiclyVisible = {0}", publiclyVisible);

    using (MemoryStream stream = new MemoryStream(data, 0, data.Length, false, publiclyVisible)) {
      // 内部バッファを取得
      byte[] buffer = stream.GetBuffer();

      Console.WriteLine("Length = {0}", buffer.Length);
      Console.WriteLine(BitConverter.ToString(buffer));
      Console.WriteLine("buffer == data : {0}", buffer == data);
    }

    Console.WriteLine();

    // publiclyVisibleにfalseを指定してMemoryStreamを作成
    publiclyVisible = false;

    Console.WriteLine("publiclyVisible = {0}", publiclyVisible);

    using (MemoryStream stream = new MemoryStream(data, 0, data.Length, false, publiclyVisible)) {
      // 内部バッファを取得 (UnauthorizedAccessExceptionがスローされる)
      byte[] buffer = stream.GetBuffer();

      Console.WriteLine("Length = {0}", buffer.Length);
      Console.WriteLine(BitConverter.ToString(buffer));
      Console.WriteLine("buffer == data", buffer == data);
    }
  }
}
実行結果
publiclyVisible = True
Length = 8
01-23-45-67-89-AB-CD-EF
buffer Is data : True

publiclyVisible = False

ハンドルされていない例外: System.UnauthorizedAccessException: MemoryStream の内部バッファーにアクセスできません。
   場所 System.IO.MemoryStream.GetBuffer()
   場所 Sample.Main()

GetBufferメソッドは、常にMemoryStreamが使用しているバッファと同一のインスタンスを返します。 従って、GetBufferメソッドで得られるバイト配列の内容を変更すると、MemoryStreamで読み込まれる結果にも影響することになります。

GetBufferメソッドで取得した内部バッファへの変更
using System;
using System.IO;

class Sample {
  static void Main()
  {
    byte[] initialData = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};

    // ストリームへの書き込みは許可しないが、バッファの取得は許可するMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(initialData, 0, initialData.Length, false, true)) {
      // 内部バッファを取得
      byte[] buffer = stream.GetBuffer();

      // 内部バッファの内容をクリアする
      Array.Clear(buffer, 0, buffer.Length);

      // Readメソッドで読み込む
      byte[] data = new byte[8];

      stream.Read(data, 0, data.Length);

      // 読み込んだ内容を表示する
      Console.WriteLine(BitConverter.ToString(data));
    }
  }
}
実行結果
00-00-00-00-00-00-00-00

§3.2 TryGetBufferメソッド

.NET Framework 4.6以降ではTryGetBufferメソッドを使うことによっても内部バッファを取得することができます。 TryGetBufferメソッドとGetBufferメソッドの違いは次の二点です。

  • バッファが取得できない場合、TryGetBufferメソッドは例外をスローせず、戻り値としてfalseを返す。
  • 取得したバッファはArraySegment<byte>としてTryGetBufferメソッドのoutパラメータに格納される。

バッファが取得できるかどうかの動作はGetBufferメソッドと同様で、MemoryStreamのコンストラクタで指定した値によって決まります。

TryGetBufferメソッドを使ってMemoryStreamの内部バッファを参照するArraySegmentを取得する
using System;
using System.IO;
using System.Linq;

class Sample {
  static void Main()
  {
    byte[] data = new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};

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

    bool publiclyVisible = true;

    // 引数publiclyVisibleにtrueを指定し、dataの一部分を参照するMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(data, 2, 4, false, publiclyVisible)) {
      // 内部バッファを参照するためのArraySegment
      ArraySegment<byte> buffer;

      // 内部バッファを取得
      if (stream.TryGetBuffer(out buffer)) {
        // 取得したバッファが参照する内容を表示する
        Console.WriteLine("Offset = {0}", buffer.Offset);
        Console.WriteLine("Count = {0}", buffer.Count);

        // BitConverter.ToStringと同じ書式でArraySegmentの内容を文字列化
        Console.WriteLine(string.Join("-", buffer.Select(b => b.ToString("X2"))));
      }
    }
  }
}
実行結果
01-23-45-67-89-AB-CD-EF
Offset = 2
Count = 4
45-67-89-AB

GetBufferメソッドでは、バッファとして使用している配列全体が返されてしまうため、取得したバッファを参照する際に誤ってMemoryStreamが参照していない領域を参照してしまう可能性があります。

一方TryGetBufferメソッドでは取得したバッファはArraySegment<byte>として返されます。 これにより、返されたArraySegmentのArrayプロパティを直接参照しない限りは、MemoryStreamが参照していない領域を誤って参照することは起こりえなくなります。

したがって、配列の一部を参照するようなMemoryStreamでは、バッファを取得する場合はGetBufferメソッドよりもTryGetBufferメソッドを使った方が誤った操作を行う可能性が低いためより安全と言えます。

ArraySegment構造体については部分配列 §.ArraySegment構造体を参照してください。

§4 バッファの破棄・クリア

MemoryStreamにはバッファの内容をクリアするStringBuilder.Clearのようなメソッドは用意されていません。 かわりに、SetLengthメソッドを使ってストリームの長さを0にすることによりMemoryStreamに書き込まれている内容を破棄することができます。

MemoryStreamのバッファの内容をクリアする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (MemoryStream stream = new MemoryStream()) {
      // streamに8バイト分書き込む
      for (int i = 0; i < 8; i++) {
        stream.WriteByte((byte)i);
      }

      Console.WriteLine("Length = {0} : {1}", stream.Length, BitConverter.ToString(stream.ToArray()));

      // SetLengthを使ってstreamに書き込んだ内容を破棄
      stream.SetLength(0);

      Console.WriteLine("Length = {0} : {1}", stream.Length, BitConverter.ToString(stream.ToArray()));

      // streamに4バイト分書き込む
      for (int i = 4; i < 8; i++) {
        stream.WriteByte((byte)i);
      }

      Console.WriteLine("Length = {0} : {1}", stream.Length, BitConverter.ToString(stream.ToArray()));
    }
  }
}
実行結果
Length = 8 : 00-01-02-03-04-05-06-07
Length = 0 : 
Length = 4 : 04-05-06-07