MemoryStreamクラス
メモリへの読み書き
FileStreamクラスの項ではストリームを利用してファイルを読み書きする方法を紹介しましたが、ファイルとして保存する必要がない場合はMemoryStreamを利用します。
MemoryStreamはストレージ(HDDやSSDなど)ではなくメモリにデータを読み書きします。
Streamに関してはStreamの項を参照してください。
このページの解説は上記ページを読んでいることが前提となっています。
このページではusingステートメントを利用したコードで記述します。
using (MemoryStream ms = new MemoryStream())
{
//読み書き処理...
}
//この時点でmsはクローズ、破棄されている
データの読み書き
ストリームへのデータの書き込みはWrite
、WriteByte
メソッドで行います。
ストリームからのデータの読み込みはRead
、ReadByte
メソッドで行います。
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型配列を指定するオーバーロードではpubliclyVisible
にtrue
を指定して生成されたインスタンスで使用可能です。
つまり、byte型配列をコンストラクター指定する場合はMemoryStream(buffer, index, count, writable, true)
のオーバーロードを使用する必要があります。
publiclyVisible
がfalse
のインスタンスで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
位置にデータをコピーします。