長年使っていた Perl+C# の WordPress 投稿ツールを、.net core を機会に F# に直そうとしている途中なのですが、そのなかでふと、リフレクションを使わずに XML-RPC を実装するのはさぞしんどいことではないか?
ということに思い至りました。いや、もともと使っていた XMLRPC.NET はインターフェース形式なのであれこれとややこしくなっているんですよね。そのあたりで使い勝手が悪くなってしまっているのと、ストレス発散ののためにも F# で書き直しをしています。
実は、MVCパターンのControllerの呼び出しにもリフレクションが使われます。ブラウザから呼び出されたURLアドレスからActionメソッドの呼び出しがそれで、Controllerクラスのメソッドをあらかじめ登録しているのではなくて、実行時にメソッドを探します。ASP.NET MVC だといちいちコンパイルをしないと駄目だけど、CakePHPのようなパターンだと動的言語なのでスクリプトを直接修正することができます。
そんなわけで、XML-RPC だと
class Prada { public int Style { get; set; } public string Color { get; set; } }
なクラスを
<struct> <member> <name>Style</name> <value> <int>10</int> </value> </member> <member> <name>Color</name> <value> <string>Pink</string> </value> </member> </struct>
な風にXML形式に直します。実際はmethodCall要素があったり、今だったらJSON形式のほうが楽だろうってことなんですが、WordpressにXMLRPCで投稿すると楽なので自作ツールを作って使っています。
リフレクションを使う
じゃあ C# でXML形式に直すことを考えると、PradaクラスのStyleプロパティとColorプロパティをどうにかして持ってこないといけないので、プロパティを列挙するリフレクションを使えばいいんだね、ということになります。
using System.Reflection; public void TestMethod1() { var m = new Prada { Style = 10, Color = "Pink" }; var ti = m.GetType().GetTypeInfo(); var plist = ti.DeclaredProperties; foreach (var prop in plist) { var n = prop.Name; var v = prop.GetValue(m); Debug.WriteLine($"prop: {n} value: {v}"); } }
こんな風に、GetTypeInfoメソッドでクラス情報を取ってきて、DeclaredPropertiesコレクションでプロパティの一覧を取得、その後で名前と値を取ってきます。GetValueメソッドで取ってきたものは object 型なので、実際に利用するときは元の型に戻さないといけません。型自体は、PropertyType プロパティで取って来れるので、これをそれぞれの型と is 演算子で比較するか、乱暴な場合は as でキャストしてしまいます。
XML-RPCのときは最終的に文字列に直してしまうので、ToString で代用させています。
ちなみに、F# で書くとこんな感じになります。
type Prada() = member val Style = 0 with get, set member val Color = "" with get, set [<TestMethod>] let TestMethod1() = let m = new Prada( Style=10, Color="Pink" ) let ti = m.GetType().GetTypeInfo() let plist = ti.DeclaredProperties for prop in plist do let n = prop.Name let v = prop.GetValue(m) System.Diagnostics.Debug.WriteLine(String.Format("prop: {0} value: {1}", n, v ))
この列挙したプロパティを使って XML 形式に直すわけですが、そのあたりは XElement を使うと楽にできるので、ちょっと省略。
で、アリプラの本題にいきます。
これって、リフレクションを使うと「クラス情報」を取れることになるけど、内部実装的にはどうなっているんだろうか。いや、そもそも、C++ のようなクラス情報を持たない言語の場合は、どうやって実装すればいいんだろうか?
という話になります。クラス情報なんだから、クラスに紐づいているわけで、new で作成したオブジェクトとは別ですよね。なので、いくつもオブジェクトを作ったとしても、クラス情報とかプロパティの型とかは一種類しかないわけです。Javascriptのようにプロトタイプで後からどんどん付け加えていくというパターンもあるので、プロパティやメソッドが固定というわけではないけれど(実装的には、後付け用のコレクションを用意するわけだし、dynamic型みたいな感じになる)、もともとの固定のプロパティやメソッドをどうやって扱おうかということになります。
リフレクションもインターフェースも使わないで実装する
なので、アリスは「リフレクション」も「インターフェース」も嫌いなので、ルイスは何か別の方法で実装しなければいけません。本来ならば、インターフェースを作ってという話になるけど、できるだけリフレクションっぽい形にしたい。あと、クラスの属性のように付加情報も取り入れたい。
class PradaNew { public static string ClassName = "PradaNew"; public static Dictionary<string, Func<PradaNew, object>> Properties = new Dictionary<string, Func<PradaNew, object>> { {"Style", m => m.Style }, {"Color", m => m.Color } }; public int Style { get; set; } public string Color { get; set; } }
そんな訳で、ルイスは「PradaNew」クラスを作って、付加情報を独自に追加しました。なんか、それっぽい形で取れるように、クラス名と、プロパティ名を渡すと値が取れるような仕組みを作っておきます。
両方とも static プロパティなので、クラスのみに紐づいた情報になりますね。
でもって、PradaNew を使ったリフレクションっぽい形の例がこっち。
public void TestMethod2() { // リフレクションを使わないパターン var m = new PradaNew { Style = 10, Color = "Pink" }; var ti = m.GetType().GetTypeInfo(); var plist = PradaNew.Properties; foreach (var prop in plist) { var n = prop.Key; var v = prop.Value(m); Debug.WriteLine($"prop: {n} value: {v}"); } }
プロパティ名を Key で、値のほうを Value で取ってくるのが多少乱暴ですが、先のリフレクションと同じようなスタイルになります。
ここにクラスの属性として「HttpGet」とか加えたいときも、ClassName プロパティのように static なプロパティを追加すれば ok ですね。static なプロパティなので、オブジェクトが大量に増えてもメモリは増えません。また、値を取ってくるところは適当にキャッシュしてやれば、通常のメソッド呼び出しと遜色ないスピードが出せそうだということがわかります。
これを、ちょっと C++ で直してみます。
#include <map> #include <functional> class Prada { private: int _style = 0; std::string _color = ""; public: int getStyle() { return _style; } void setStyle(int v) { _style = v; } std::string getColor() { return _color; } void setColor(std::string v) { _color = v; } public: const std::string className = "Prada"; static std::map <std::string, std::function<void*(Prada*)>> properties; Prada() { auto func = [](Prada* m) { auto v = m->getStyle(); }; Prada::properties.insert(std::make_pair(std::string("Style"), [](Prada* m) { auto v = m->getStyle(); return &v; })); Prada::properties.insert(std::make_pair(std::string("Color"), [](Prada* m) { auto v = m->getColor(); return &v; })); } }; std::map <std::string, std::function<void*(Prada*)>> Prada::properties;; int main() { auto m = new Prada(); m->setStyle(10); m->setColor("Pink"); auto plist = m->properties; for each (auto prop in plist) { auto n = prop.first; auto v = prop.second; void* x = v(m); // void* から元の型へ戻すには? } return 0; }
元の型に戻すのには、プロパティの型を調べる必要があるので、もう少し手を入れないといけないけど、だいたいはこんな感じ。map にしてラムダ式を返すようにしたのでややこしいことになっていますが、構造体を作ってプロパティの型を調べるところと、値を取得するところを作ったほうが楽かも。
まあ、こんな実装は普通はしないわけで、内部的にはこんなことになっているなーと思いつつ、リフレクションを道具として使うとよいのではと思った次第。