MemoryStreamクラス

メモリへの読み書き

FileStreamクラスの項ではストリームを利用してファイルを読み書きする方法を紹介しましたが、ファイルとして保存する必要がない場合はMemoryStreamを利用します。
MemoryStreamはストレージ(HDDやSSDなど)ではなくメモリにデータを読み書きします。

Streamに関してはStreamの項を参照してください。
このページの解説は上記ページを読んでいることが前提となっています。

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


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

データの読み書き

ストリームへのデータの書き込みはWriteWriteByteメソッドで行います。
ストリームからのデータの読み込みはReadReadByteメソッドで行います。


byte[] bytesNum1 = BitConverter.GetBytes(12345);
byte[] bytesStr1 = Encoding.Unicode.GetBytes("あいうえお");
byte b1 = 123;

byte[] bytesNum2 = BitConverter.GetBytes(0);
byte[] bytesStr2 = Encoding.Unicode.GetBytes("abcde");
byte b2 = 0;

using (MemoryStream ms = new MemoryStream())
{
    //配列全体を書き込み
    ms.Write(bytesNum1, 0, bytesNum1.Length);

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

    //1バイトを書き込み
    ms.WriteByte(b1);

    //ストリームの位置を先頭にセット
    ms.Position = 0;

    int read;

    //配列の要素数分を読み込み
    read = ms.Read(bytesNum2, 0, bytesNum2.Length);

    //6バイトを読み取り配列の2番目の要素から格納
    read = ms.Read(bytesStr2, 2, 6);

    //1バイト読み取り
    b2 = (byte)ms.ReadByte();
}

Console.WriteLine(BitConverter.ToInt32(bytesNum2, 0));
Console.WriteLine(Encoding.Unicode.GetString(bytesStr2));
Console.WriteLine(b2);

Console.ReadLine();
12345
aいうえe
123

Writeメソッドはbyte型配列をストリームに書き込みます。
第一引数はストリームに書き込む配列を指定します。
第二引数は配列のコピー開始位置を指定します。
第三引数は書き込むバイト数を指定します。
戻り値はありません。

WriteByteメソッドは1バイトのデータをストリームに書き込みます。
戻り値はありません。

Readメソッドは指定のバイト数をストリームから読み取り配列に格納します。
第一引数はデータを格納する配列を指定します。
第二引数は配列のコピー開始位置を指定します。
第三引数は読み取るバイト数を指定します。
戻り値は実際に読み取れたバイト数(int型)で、「0」が返ってくるとストリームの終端となります。

ReadByteメソッドは1バイトのデータをストリームから読み取り、int型で返します。
「-1」が返ってくるとストリームの終端となります。

これらはFileStreamクラスと共通なので詳しくはそちらを参照してください。

コンストラクター

MemoryStreamのコンストラクターにはいくつかのオーバーロードがあります。

オーバーロード 説明 書き込み サイズ変更
MemoryStream() 空のストリームを生成
MemoryStream(int capacity) 初期サイズを指定して空のストリームを生成
MemoryStream(byte[] buffer) bufferに対するストリームを生成 ×
MemoryStream(byte[] buffer, bool writable) bufferに対するストリームを生成 writableで指定 ×
MemoryStream(byte[] buffer, int index, int count) buffer[index]~buffer[index+count-1]までの範囲に対するストリームを生成 ×
MemoryStream(byte[] buffer, int index, int count, bool writable) buffer[index]~buffer[index+count-1]までの範囲に対するストリームを生成 writableで指定 ×
MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible) buffer[index]~buffer[index+count-1]までの範囲に対するストリームを生成
publiclyVisibleをtrueにするとGetBufferメソッドでバイト配列が得られる
writableで指定 ×

引数のcapacityはCapacityプロパティの初期サイズを指定してMemoryStreamを生成します。

第一引数にbyte型配列を指定するオーバーロードは、そのbyte型配列を直接操作するMemoryStreamを生成します。
MemoryStreamでデータを変更すると元のbyte配列のデータも変更されます。
MemoryStreamのサイズは変更不可となります。
writableをfalseに指定すると読み取り専用となります。

Capacityプロパティ

MemoryStreamはメモリ上にデータが保存されますが、実際のデータが保存されているメモリ領域のほかに、使用されていない空き領域があります。
実際のデータサイズはLengthプロパティで取得できます。
MemoryStreamが確保しているメモリ領域全体のサイズはCapacityプロパティで取得/設定ができます。

Lengthプロパティの値がCapacityプロパティの値を超えると、新しいメモリ領域が確保されます。
メモリの確保はそこそこコストの掛かる処理なので、使用するデータサイズがあらかじめ分かっている場合は最初にそのサイズのCapacityを確保しておくと高速化が可能です。
反対に、それほど大きなサイズを使用しない場合は小さめのサイズを確保しておくとメモリの節約ができます。


//Capacityを1024に設定
using (MemoryStream ms = new MemoryStream(1024))
{
    for(int i = 0; i < ms.Capacity; ++i)
    {
        ms.WriteByte((byte)i);
    }

    Console.WriteLine(ms.Length);
}
1024

CapacityプロパティにLengthよりも小さい値を設定すると例外(ArgumentOutOfRangeException)が発生します。

メソッド

バイト型配列に変換

MemoryStreamはToArrayメソッドでデータをバイト型配列に変換できます。


byte[] bytesNum1 = BitConverter.GetBytes(12345);
byte[] bytesStr1 = Encoding.Unicode.GetBytes("あいうえお");
byte b1 = 123;

byte[] bytes;

using (MemoryStream ms = new MemoryStream())
{
    ms.Write(bytesNum1, 0, bytesNum1.Length);
    ms.Write(bytesStr1, 2, 6);
    ms.WriteByte(b1);

    //MemoryStreamをbyte型配列に変換
    bytes = ms.ToArray();
}

File.WriteAllBytes("test.bin", bytes);

このサンプルコードはMemoryStreamの内容をbyte型配列に変換し、ファイルに書き出しています。
なお、戻り値はストリームの内容のコピーなので配列を編集してもストリームに影響はありません。

データの消去

MemoryStreamに書き込んだデータを消去するにはSetLengthメソッドの引数に0を指定して実行します。


using (MemoryStream ms = new MemoryStream())
{
    ms.Write(new byte[] { 1, 2, 3 }, 0, 3);
    foreach(var b in ms.ToArray())
        Console.Write("{0} ", b);
    Console.WriteLine();

    //全て消去
    ms.SetLength(0);

    ms.Write(new byte[] { 7, 8, 9 }, 0, 3);
    foreach (var b in ms.ToArray())
        Console.Write("{0} ", b);
}
1 2 3 
7 8 9 

ただしCapacityはそのままなので、必要に応じて値を変更してください。

ストリームの長さの拡張

MemoryStreamでは、SetLengthメソッドで長さを拡張した場合や、Positionプロパティをストリームの末尾より後ろにセットしデータを書き込むことでストリームの長さを拡張した場合、拡張された新しい領域は0で初期化されます。

内部バッファの取得

MemoryStreamが内部に確保しているデータはGetBufferメソッドで取得できます。


//byte型配列の要素の値を二倍にする
static void DoubleElementsValue(byte[] arr)
{
    for (int i = 0;i < arr.Length; ++i)
    {
        arr[i] *= 2;
    }
}

static void Main(string[] args)
{
    byte[] bytes = new byte[] { 1, 2, 3, 4 };
    using (MemoryStream ms = 
        new MemoryStream(bytes, 0, bytes.Length, true, true))
    {
        //ストリーム内部の配列を取得
        byte[] buffer = ms.GetBuffer();
        DoubleElementsValue(buffer);

        //ストリーム内部の配列のコピーを取得
        byte[] toArray = ms.ToArray();
        for (int i = 0; i < toArray.Length; ++i)
        {
            Console.Write("{0} ", toArray[i]);
        }
    }
}
2 4 6 8

ToArrayメソッドはMemoryStreamの内容をコピーしたbyte配列が返されますが、GetBufferメソッドはストリームの操作対象のbyte配列そのものを返します。
つまり戻り値のbyte配列のデータを変更するとストリームのデータも変更されます。
MemoryStream以外の方法でbyte配列を操作するような場合や、ToArrayメソッドのコピーに掛かるコストを省く場合に使用します。

ToArrayメソッドはLengthプロパティのサイズのbyte型配列を返しますが、GetBufferメソッドはCapacityプロパティのサイズのbyte型配列を返します。
つまりストリームが使用していない空き領域を含みます。
空き領域に変更を加えてもストリームの長さは変更されないため、ストリームには反映されません。

GetBufferメソッドは、空のコンストラクター、Capacityを指定するオーバーロード、およびbyte型配列を指定するオーバーロードではpubliclyVisibletrueを指定して生成されたインスタンスで使用可能です。
つまり、byte型配列をコンストラクター指定する場合はMemoryStream(buffer, index, count, writable, true)のオーバーロードを使用する必要があります。
publiclyVisiblefalseのインスタンスでGetBufferメソッドを呼び出すと例外(UnauthorizedAccessException)が発生します。

ストリームサイズが変更可能な場合に、GetBufferメソッドでbyte型配列を取得した後にCapacityプロパティの値が変更される操作をすると、そのbyte型配列はストリームから切り離されます。
(ストリームは新しいメモリ領域を確保するため、以前のメモリ領域は保持しなくなる)
再度GetBufferメソッドを実行した場合は新しい(別のメモリ領域である)byte型配列が取得されますし、以前に取得したbyte型配列の要素を変更してもストリームには影響しません。

「バッファ」とは、データを一時的に保存すること、またはそのための領域を言います。

TryGetBufferメソッド

TryGetBufferメソッドは、MemoryStreamの内部バッファが取得可能ならば取得します。
このメソッドは.NET Framework4.6から使用可能です。


//配列の要素の値を二倍にする
static void DoubleElementsValue(byte[] arr)
{
    for (int i = 0;i < arr.Length; i++)
    {
        arr[i] *= 2;
    }
}

static void Main(string[] args)
{
    using (MemoryStream ms = new MemoryStream(8))
    {
        //適当なデータの書き込み
        for(int i = 0; i < ms.Capacity; ++i)
        {
            ms.WriteByte((byte)i);
        }

        ArraySegment<byte> seg;
        if(ms.TryGetBuffer(out seg)) //真
        {
            DoubleElementsValue(seg.Array);
        }

        //内容の表示
        ms.Position = 0;
        for (int i = 0; i < ms.Capacity; ++i)
        {
            Console.Write("{0} ", ms.ReadByte());
        }
    }
    Console.WriteLine();

    byte[] bytes = new byte[8];
    using (MemoryStream ms = new MemoryStream(bytes))
    {
        //適当なデータの書き込み
        for (int i = 0; i < ms.Capacity; ++i)
        {
            ms.WriteByte((byte)i);
        }

        ArraySegment<byte> seg;
        if (ms.TryGetBuffer(out seg)) //偽
        {
            //実行されない
            DoubleElementsValue(seg.Array);
        }

        //内容の表示
        ms.Position = 0;
        for (int i = 0; i < ms.Capacity; ++i)
        {
            Console.Write("{0} ", ms.ReadByte());
        }
    }
}
0 2 4 6 8 10 12 14
0 1 2 3 4 5 6 7

TryGetBufferメソッドの引数にはArraySegment<byte>型の変数をout参照渡しで指定します。
ArraySegmentというのは特定の配列の一部に対する参照を保持する構造体です。
参照なので、ArraySegmentを通して要素を編集すると元の配列にも影響します。
ジェネリックに対応しているので、ここではMemoryStreamが操作するbyte型のArraySegmentを使用します。

TryGetBufferメソッドの戻り値はbool型で、バッファが取得できた場合に真を返します。
取得に成功した場合、ArraySegment構造体のArrayプロパティがバッファへの参照を保存しているので、これを通して元の配列の操作が可能です。

MemoryStreamのインスタンスからはバッファが取得可能か否かを調べる方法がないので、GetBufferメソッドだけでは例外が発生するか否かで判定するしかありませんが、TryGetBufferを使用すれば低い実行コストでバッファの取得を試みることができます。

MemoryStreamのコピー

MemoryStreamを別のストリームにコピーするにはWriteToメソッドを使用します。


byte[] bytesNum = BitConverter.GetBytes(12345);
byte[] bytesStr = Encoding.Unicode.GetBytes("あいうえお");
byte b = 123;

string output = "test.bin";

using (MemoryStream ms = new MemoryStream())
using (FileStream fs = new FileStream(output, FileMode.Create))
{
    ms.Write(bytesNum, 0, bytesNum.Length);
    ms.Write(bytesStr, 2, 6);
    ms.WriteByte(b);

    ms.WriteTo(fs);
}

コピー先のストリームの種類は(ストリームに書き込み可能であれば)問いません。
このコードは最後の行でMemoryStreamの内容をそのまま読み書きモードで開いているFileStreamにコピーしています。
つまりMemoryStreamの内容が指定のファイルに書き出されます。

StreamクラスのCopyToメソッドは、現在のPositionの位置からのコピーが行われますが、このメソッドはMemoryStreamの全体がコピーされます。

MemoryStreamを利用したストリームのコピー

MemoryStream以外のStreamを別のStreamにコピーする方法を紹介します。

.Net Framework4.0以降ではStreamクラスにCopyToメソッドが追加されているので、Streamのコピーはこれを使用するのが簡単です。
それ以前のバージョンでは以下のようなコードでコピーします。


/// <summary>
/// ストリームをコピーする。
/// </summary>
/// <param name="from">コピー元のストリーム</param>
/// <param name="to">コピー先のストリーム</param>
static void CopyStream(Stream from, Stream to)
{
    if (from == null || !from.CanRead || !from.CanSeek ||
        to == null || !to.CanWrite)
        return;

    long pos = from.Position;
    byte[] buf = new byte[65536];
    while (true)
    {
        //読み取ったバイト数を取得
        int read = from.Read(buf, 0, buf.Length);
        if (read == 0)
            break;

        //読み取ったバイト数分を書き込み
        to.Write(buf, 0, read);
    }
    from.Position = pos;
}

static void Main(string[] args)
{
    string input = "test.bin";
    string output = "test_output.bin";

    using (var fsIn = new FileStream(input, FileMode.Open))
    using (var fsOut = new FileStream(output, FileMode.Create))
    {
        CopyStream(fsIn, fsOut);
    }
    Console.WriteLine("終了します。");

    Console.ReadLine();
}

自作メソッドのCopyStreamは、コピー元ストリームのPosition位置からコピー先のストリームのPosition位置にデータをコピーします。