2015/12/18

MemoryStreamのデータ

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

1. 前置き


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

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

2. MemoryStream.ToArrayメソッド


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

例えば、こんな感じ。
この例ではDataContractSerializerでオブジェクトをシリアライズしたデータをMemoryStreamに詰め、ToArrayメソッドで取り出したバイト配列をEncoding.GetStringメソッドに渡しています。

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

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

3. MemoryStream.GetBufferメソッド


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

例えば、開始位置を0に決め打ちすれば、こんな感じになります。
問題は開始位置、すなわち_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プロパティでデータの位置を確定できるわけです。

これを使うと以下のようにできます。
これでコンストラクターに左右されず、安全にコピーなしでデータを取り出すことができるようになりました。

5. まとめ


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