「メタプログラミング .NET」をざっと読み終わったので、手元の github から ExDoc をダウンロードしてきて、dynamic 版の ExDoc を作っているところです。
旧 ExDoc は、演算子のオーバーライトと暗黙のキャストを使って、/,*,%演算子を使って XML を探索できていました。まあ、奇妙といえば奇妙だけど、LINQ の派生みたいな感じで作れるわけです。タグを指定するのが文字列なのがあれなのと、だったら XPath で指定してもいいんじゃないか、という話もありますが、まあ実験的に。ちなみに、このライブラリは実運用で4年ほど使っています。CakePHP をサーバーにして、クライアント側の XML パースを ExDoc を使ってパースしているという技ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void TestQuery() { var xml = "<root>" + "<person id='1'>masuda</person>" + "<person id='2'>tomoaki</person>" + "<person id='3'>yumi</person>" + "</root>" ; var doc = ExDocument.LoadXml(xml); // query element ExElement el = doc * "person" % "id" == "2" ; Assert.AreEqual( "tomoaki" , el.Value); // query elements ExElements els = doc * "person" ; Assert.AreEqual(3, els.Count); Assert.AreEqual( "masuda" , els[0].Value); Assert.AreEqual( "tomoaki" , els[1].Value); Assert.AreEqual( "yumi" , els[2].Value); } |
で、常々、PHP の SimepleXML がうらやましかった訳ですが、そんな風に直接プロパティで設定できるのが、dynamic の良いところです。まあ、インテリセンスが効かなくなってしまうのですが、どうせ XML タグ名なのだから、そこは割り切りということで。dynamic で受けて、指定の型へキャストし直して使う方法もあるので、それもおいおいと実装する予定。「メタプログラミング .NET」にも書いてあるけど、複数のインターフェースを使って、片方のインターフェースを隠す技が使えます。確か、Xamarin.Forms の XAML ツリーのところで使っています。
dynamic 版の ExDoc を使うと、こんな風に XML タグをプロパティのように扱えます。属性に連想配列を使っているのは、SimpleXML の真似でもあるのと、doc.person.@id のようにはできなかったからなのです。先頭の @ は有効になるんだけど、TryGetMember メンバでだと、「id」としか取れないので、「@id」と「id」は同じ名前として扱われるためです。残念。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void TestQuery() { var xml = @" <root> <person id='1'>masuda</person> <person id='2'>tomoaki</person> <person id='3'>yumi</person> </root> " ; var doc = ExDocument.LoadXml(xml); // query element var el = doc.person[ "id" ] == "2" ; Assert.AreEqual( "tomoaki" , el.Value ); // query elements var els = doc.person; Assert.AreEqual(3, els.Count); Assert.AreEqual( "masuda" , els[0].Value); Assert.AreEqual( "tomoaki" , els[1].Value); Assert.AreEqual( "yumi" , els[2].Value); } |
まあ、それでも、連想配列は属性で、配列は子要素で示せるわけで、なかなかいい感じでパースができます。
子孫要素も含めて調べるときは「*」演算子のかわりに、タグ名に「_」を付けます。プロパティ名やメソッド名を後付けでできる(実行時に内部で分解して分岐させる)のが dynamic の良いところです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public void TestSelectAttr() { var xml = @" <html> <body> <a id='a1' href='link001.html'>title</a> <a id='a2' href='link002.html'>title</a> </body> </html> " ; var st = new StringReader(xml); var doc = XDocument.Load(st); var edoc = ExDocument.Load(doc); var el = edoc._a[ "id" ] == "a2" ; Assert.AreEqual( "title" , el.Value); Assert.AreEqual( "link002.html" , el[ "href" ]); } |
これらをどう実装しているかというと、参照だけならば以下で ok です。
– System.Dynamic.DynamicObject を継承する
– TryGetMember を実装する
– TryGetIndex を実装する
– == 演算子を再定義する。
まあ、先行きは値の設定までやるので、TrySetMember や TrySetIndex も実装していきます。
具体的な説明は、.net core の DynamicObject のコードを読むか(コードに詳しい説明が書いてあります)、「メタプログラミング .NET」にざっと書いてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | public class ExElement : System.Dynamic.DynamicObject { public string TagName { get { return _el.Name.LocalName; } } public string Name { get { return TagName; } } public string Value { get { return _el.Value; } } internal XElement _el = null ; public ExElement( XElement el = null ) { _el = el; } public override bool TryGetMember(GetMemberBinder binder, out object result) { var els = new ExElements(); if (binder.Name[0] == '_' ) { string name = binder.Name.Substring(1); els = SelectNodes(_el, els, name, true ); } else { els = SelectNodes(_el, els, binder.Name); } result = els; return true ; } private ExElements SelectNodes( XElement x, ExElements els, string name, bool deep = false ) { foreach ( var el in x.Elements()) { if (name == el.Name) { els.Add( new ExElement(el)); } if (deep) els = SelectNodes(el, els, name, true ); } return els; } public override bool TryGetIndex(GetIndexBinder binder, object [] indexes, out object result) { result = ExDocument.EmptyElement; if (indexes.Count() == 1) { if (indexes[0] is int ) { int i = ( int )indexes[0]; if (i < _el.Parent.Elements().Count()) { result = new ExElement(_el.Parent.Elements().ToList()[i]); } } else if (indexes[0] is string ) { string s = indexes[0] as string ; var attr = _el.Attribute(s); if (attr != null ) { result = new ExAttr(attr); } } } return true ; } /// <summary> /// 探索 /// </summary> /// <param name="lst"></param> /// <param name="value"></param> /// <returns></returns> public static ExElements operator ==(ExElement el, string value) { var result = new ExElements(); foreach ( var it in el._el.Elements()) { if (it.Value == value) { result.Add( new ExElement(it)); } } return result; } public static ExElements operator !=(ExElement el, string value) { var result = new ExElements(); foreach ( var it in el._el.Elements()) { if (it.Value != value) { result.Add( new ExElement( it)); } } return result; } public override bool Equals( object obj) { return base .Equals(obj); } public override int GetHashCode() { return base .GetHashCode(); } /// <summary> /// 文字列にキャスト /// </summary> /// <param name="el"></param> public static implicit operator string (ExElement el) { return el.Value; } /// <summary> /// 数値にキャスト /// </summary> /// <param name="el"></param> public static implicit operator int (ExElement el) { return int .Parse(el.Value); } /// <summary> /// 実数にキャスト /// </summary> /// <param name="el"></param> public static implicit operator double (ExElement el) { return double .Parse(el.Value); } } |
もうちょっと整理して、前バージョンの ExDoc と同じように使えたら NuGet にアップしましょう。