「メタプログラミング .NET」をざっと読み終わったので、手元の github から ExDoc をダウンロードしてきて、dynamic 版の ExDoc を作っているところです。
旧 ExDoc は、演算子のオーバーライトと暗黙のキャストを使って、/,*,%演算子を使って XML を探索できていました。まあ、奇妙といえば奇妙だけど、LINQ の派生みたいな感じで作れるわけです。タグを指定するのが文字列なのがあれなのと、だったら XPath で指定してもいいんじゃないか、という話もありますが、まあ実験的に。ちなみに、このライブラリは実運用で4年ほど使っています。CakePHP をサーバーにして、クライアント側の XML パースを ExDoc を使ってパースしているという技ですね。
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」は同じ名前として扱われるためです。残念。
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 の良いところです。
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」にざっと書いてあります。
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 にアップしましょう。