2015/12/18

MemoryStreamのデータ

MemoryStreamといえばStream系の処理をするときのバッファー役ですが、これからのデータの取り出し方について。

1. 前置き


実際のコードを見た方が手っ取り早いので、随時Reference SourceのMemoryStreamのソースを参照していきます。

MemoryStreamはメモリ上の領域をバッキングストアとしたStreamで、そのためのバイト配列を内部に持っています。それがプライベートフィールドの_bufferです。その中の実際のデータの位置は_origin_lengthで示されます。つまり、_bufferの全部がデータだとは限らず、その一部に格納される構造になっています。

2. MemoryStream.ToArrayメソッド


MemoryStreamに何らかのデータを詰めたとして、それをStreamとして受け渡すのではなく、直接取り出したい場合、オーソドックスなのはMemoryStream.ToArrayメソッドです。

例えば、こんな感じ。
public string GetSerializedString0<T>(T source)
{
using (var ms = new MemoryStream())
{
// Fill the MemoryStream with some data (example).
var serializer = new DataContractSerializer(typeof(T));
serializer.WriteObject(ms, source);
// MemoryStream.ToArray method makes a copy of inner byte array and
// returns the copy. This copying is not always desirable.
return Encoding.UTF8.GetString(ms.ToArray());
}
}
この例ではDataContractSerializerでオブジェクトをシリアライズしたデータをMemoryStreamに詰め、ToArrayメソッドで取り出したバイト配列をEncoding.GetStringメソッドに渡しています。

このToArrayメソッドのソースを見ると、データの長さ(_length - _origin)と同じ長さのバイト配列を用意し、これにBuffer.InternalBlockCopyメソッド(たぶんBuffer.BlockCopyと同じ)で_buffer中のデータをコピーした後、この配列を返しています。

これは安全な方法ですが、処理の最後にデータを取り出したいだけの場合にはコピーが無駄だったりします。

3. MemoryStream.GetBufferメソッド


これが何とかならないかなと思っていたとき、ヒントをいただいた気がしたのでよく調べたらMemoryStream.GetBufferメソッドが存在しました。このGetBufferメソッドのソースを見ると一目瞭然ですが、_bufferの参照をそのまま返しています。ただし、この中のデータの位置は自分で指定する必要があります。

例えば、開始位置を0に決め打ちすれば、こんな感じになります。
public string GetSerializedString1<T>(T source)
{
using (var ms = new MemoryStream())
{
// Fill the MemoryStream with some data (example).
var serializer = new DataContractSerializer(typeof(T));
serializer.WriteObject(ms, source);
// MemoryStream.GetBuffer method allows access to inner byte array but
// the first index and the length must be specified. The first index
// will be 0 in many cases but it is not guaranteed.
return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length);
}
}
問題は開始位置、すなわち_originが0ではない場合で、その場合はズレが生じます。かつ、_originを外部から参照できるプロパティがありません。一応Lengthプロパティのソースを見ると、これは_length - _originの値なので、_originが参照できさえすればそのまま使えるのですが。

一応、この例のようにMemoryStreamのコンストラクターのうち元となるバイト配列のないオーバーロードを使ったときは、_originは0になるようなので、そこに注意すれば問題はなさそうですが、一抹の不安が残ります。

また、publiclyVisibleを指定できるコンストラクターでこれをfalseにした場合(_exposableがfalseの場合)、GetBufferメソッドは例外を吐きます。

4. MemoryStream.TryGetBufferメソッド


ではどうするかというところで、メソッド群を見直すとMemoryStream.TryGetBufferメソッドが存在しました。これはoutでArraySegment<byte>を返します。

このTryGetBufferメソッドのソースを見ると、ArraySegmentのコンストラクターのoffsetに_originの値を、countに_length - _originの値を指定しています。したがって、返されたArraySegmentのOffsetとCountプロパティでデータの位置を確定できるわけです。

これを使うと以下のようにできます。
public string GetSerializedString2<T>(T source)
{
using (var ms = new MemoryStream())
{
// Fill the MemoryStream with some data (example).
var serializer = new DataContractSerializer(typeof(T));
serializer.WriteObject(ms, source);
// MemoryStream.TryGetBuffer method gives a ArraySegment the reference
// to inner byte array, the first index and the length.
ArraySegment<byte> buff;
if (!ms.TryGetBuffer(out buff))
return null;
// The first index is provided by the ArraySegment.
return Encoding.UTF8.GetString(buff.Array, buff.Offset, buff.Count);
}
}
これでコンストラクターに左右されず、安全にコピーなしでデータを取り出すことができるようになりました。

5. まとめ


MemoryStreamの振る舞いはどのコンストラクターでどのように指定するかで細かく変わってくるので、GetBufferで済むか、TryGetBufferで安全策を取るか、別にToArrayで構わないか、結局はケースバイケースなのですが、どんなオプションがあるか知っておくと少し安心できます。