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

2015/11/28

Windows 10 1511のバージョン判定

Windows 10のビルド10586というか、Threshold2というか、1151のバージョンをコード的にどう確認するかについて、以下の記事を参考にレジストリ情報を追加してみた。
レジストリを読んでいるだけだが、新しいReleaseIdに「1511」の値が出ている。BuildLabの方には「th2」の文字が入っている。その後ろにある「151112-1900」は2015/11/12 1900というリリース日時を示しているのかもしれない。

これを元に実用的な処理を組むにはあやふや過ぎるので、あくまで参考程度で。

2015/11/14

WPFのPer-Monitor DPIサポート(その1)

WPF自体のPer-Monitor DPIサポートについて。


従来、WPF自体にはPer-Monitor DPIのサポートはなく、Win32 APIを駆使する必要があったわけですが、.NET Framework 4.6.1の後のバージョンでようやくサポートが入るようです。
このエントリ中にあるサーベイに登録すればプレビュー版を試せるとのことで、登録しておいたところ、昨日招待メールが届いたのでダウンロードして試してみました。

ダウンロードできるのは.NET Framework 4.6.1のプレビュー版のようで、それを利用するサンプルコードは既にGitHubで公開されています。
このサンプルコードの範囲内で簡単にまとめると、以下が追加されています。
  • System.Windows名前空間
    • DpiScaleInfo構造体(DPI情報を格納する)

  • System.Windows.Media.VisualTreeHelperクラス
    • public static DpiScaleInfo GetDpi(Visual visual)メソッド
    • public static void SetRootDpi(Visual visual, DpiScaleInfo dpiInfo)メソッド

  • System.Windows.Windowクラス
    • protected override void OnDpiChanged(DpiScaleInfo oldDpiScaleInfo, DpiScaleInfo newDpiScaleInfo)メソッド

  • System.Windows.Controls.Imageクラス
    • protected override void OnDpiChanged(DpiScaleInfo oldDpiScaleInfo, DpiScaleInfo newDpiScaleInfo)メソッド
    • public event RoutedEventHandler DpiChangedイベントハンドラー
さらにコメントを見ると、大元のSystem.Windows.Media.VisualクラスにOnDpiChangedメソッドとDpiChangedイベントハンドラーが追加されるようです。

つまり、他の色々なイベント処理と同じように、各コントロールでDPI変化イベントを捉えて処理できるようになる、ということです。当然コントロールによって予め処理が作り込まれているものもあって、Windowクラスではモニターをまたがって移動したときに自動的にサイズ変更がされるようになっていました(位置はWM_DPICHANGED準拠)。さらに必要な処理があれば、OnDpiChangedをオーバーライドするなりDpiChangedに登録するなりして入れ込めばいいわけですね。

どんなものが来るのかなと思ってましたが、自然な拡張のようでとりあえず安心しました。

[追記1]

DpiChangedイベントハンドラーはRoutedEventHandlerとなっているとおり、引数はただのRoutedEventArgsなので、DPI情報を直接取得はできません。これは少しだけ面倒なので、以下のような専用のDpiChangedEventArgsを使うようにしてはどうかとフィードバックしてみました。

[追記2]

フィードバックへの返信で、このサポートが入るのは4.6.1ではなく、その後のバージョンだと訂正されたので、修正しました。