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で構わないか、結局はケースバイケースなのですが、どんなオプションがあるか知っておくと少し安心できます。