「メタプログラミング .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 にアップしましょう。
