TypeAdapter を作る

はじめに

本記事は PowerShell Advent Calendar 2018 の 11 日目です。
PowerShell Advent Calendar 2018 は寄稿して頂ける方を絶賛募集中です。よろしくお願いいたします。
qiita.com

TypeAdapter とは何ぞや

以前の記事で、types.ps1xml について書きました。
tech.blog.aerie.jp

types.ps1xml には、<TypeAdapter> という要素があるのですが、今回はこれの使い方です。

PowerShell では、.NET のオブジェクトにないメンバーを後から足すことができます。
このために PowerShell は以下の 2 つの方法を用意しています。

  • TypeData
  • TypeAdapter

TypeData というのは、types.ps1xml や Update-TypeData コマンドを使う方法のことです。
もう一つが TypeAdapter です。

こちらの記事でもちょっとだけ触れています。
tech.blog.aerie.jp

イメージを掴む

以下のようなコードを考えてみましょう。

PS> [xml] '<foo>hello</foo>'

foo
---
hello

PS> [xml] '<bar>world</bar>'

bar
---
world

型はどちらも XmlDocument ですが、前者は foo というプロパティを持ち、後者は bar というプロパティを持ちます。
TypeData による拡張では、型に対して追加するメンバーの集合は固定的なので、このように、データの中身に応じて異なるプロパティを持たせるようなことはできません。
プロパティの数や名前を動的に変更したい場合に使うのが TypeAdapter ということでよいと思います。

TypeAdapter の作り方

TypeAdapter は PSPropertyAdapter クラスを継承したオブジェクトです。

GetTypeNameHierarchy メソッド以外のすべてのメソッドをオーバーライドする必要があります。*1
GetTypeNameHierarchy メソッドは、前回説明した型階層を返すメソッドだと思えばよいでしょう。

TypeAdapter によって追加されたプロパティは PSAdaptedProperty クラスで表されます。
TypeAdapter の仕事は、オブジェクトに対して追加する PSAdaptedProperty を返したり、そのプロパティの値や情報を返すことです。

手っ取り早くサンプルを載せてしまいましょう。特に難しいことはしていません。
やっていることは、文字列に対して、先頭から最大で 3 文字までを '0', '1', '2' というプロパティ名で返すという、実用性の欠片もないものです。


gist067fbec25b4682bf49c484c39f953eb6

TypeAdapter の使い方

モジュールに組み込んで配布する場合は、冒頭でも書きましたが、types.ps1xml の <TypeAdapter> に TypeAdapter の型を書きます。
上記のサンプルであればこんな感じ。

...
<Type>
  <Name>System.String</Name>
  <TypeAdapter>
    <TypeName>MyTypeAdapter.StringPropertyAdapter</TypeName>
  </TypeAdapter>
  ...
</Type>
...

スクリプトでやるなら Update-TypeData コマンドを使います。

Update-TypeData -TypeName 'System.String' -TypeAdapter ([MyTypeAdapter.StringPropertyAdapter]) -Force

TypeAdapter を含むアセンブリPowerShell モジュールを兼ねないのであれば、モジュール マニフェスト (*.psd1) の RequiredAssemblies にファイル名を書くなり、Add-Type コマンドを使うなりして読み込んでおく必要があるでしょう。

実装上の注意点

今回、PSAdaptedProperty を継承した CustomProperty というクラスを作っています。これは何故でしょうか。

PSAdaptedProperty クラスは public なコンストラクタを持ちますので、派生クラスを作らなくても、普通にインスタンスを作ることができます。
しかし、こうして作った PSAdaptedProperty インスタンスは、ほとんどのメンバーが機能しません。

機能しないメンバーは、以下の通り。

  • BaseObject
  • IsGettable
  • IsSettable
  • TypeNameOfValue
  • Value

PowerShellソースコードを見るとわかるのですが、PSAdaptedProperty の public なコンストラクタは、基底クラスである PSProperty のコンストラクタの引数 adapter と baseObject に null を渡しています。
上記の「機能しないメンバー」のうち、BaseObject は baseObject を返すだけなので常に null が返されますし、それ以外のプロパティはすべて adapter に移譲されますので、アクセスすると NullReferenceException が飛びます。

やる気を感じられない API デザインですが、そういうものなのでしょうがない。
というわけで(オーバーライドできない BaseObject を除いて)他のプロパティを機能するようにオーバーライドしているわけです。

ちなみに

こんなコマンドを叩いてみると…

PS> Get-TypeData | ? { $_.TypeAdapter } | Select-Object TypeName, TypeAdapter

デフォルトでは結果は空です。
つまり、PowerShell に最初から組み込まれている型定義の中で、TypeAdapter が設定されているものはありません。

じゃあ、XmlDocument はどうなってるんだと思われるかもしれません。
詳細は割愛しますが、内部的には PSPropertyAdapter とは異なる別の仕組みで動いています。

*1:GetTypeNameHierarchy メソッドも必要であればオーバーライドできます