dynamic を利用して ExDoc を書き直してみる(1)

「メタプログラミング .NET」をざっと読み終わったので、手元の github から ExDoc をダウンロードしてきて、dynamic 版の ExDoc を作っているところです。

旧 ExDoc は、演算子のオーバーライトと暗黙のキャストを使って、/,*,%演算子を使って XML を探索できていました。まあ、奇妙といえば奇妙だけど、LINQ の派生みたいな感じで作れるわけです。タグを指定するのが文字列なのがあれなのと、だったら XPath で指定してもいいんじゃないか、という話もありますが、まあ実験的に。ちなみに、このライブラリは実運用で4年ほど使っています。CakePHP をサーバーにして、クライアント側の XML パースを ExDoc を使ってパースしているという技ですね。

[code lang=”csharp”]
public void TestQuery()
{
var xml = “” +
“masuda” +
“tomoaki” +
“yumi” +
“
“;

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);
}
[/code]

で、常々、PHP の SimepleXML がうらやましかった訳ですが、そんな風に直接プロパティで設定できるのが、dynamic の良いところです。まあ、インテリセンスが効かなくなってしまうのですが、どうせ XML タグ名なのだから、そこは割り切りということで。dynamic で受けて、指定の型へキャストし直して使う方法もあるので、それもおいおいと実装する予定。「メタプログラミング .NET」にも書いてあるけど、複数のインターフェースを使って、片方のインターフェースを隠す技が使えます。確か、Xamarin.Forms の XAML ツリーのところで使っています。

dynamic 版の ExDoc を使うと、こんな風に XML タグをプロパティのように扱えます。属性に連想配列を使っているのは、SimpleXML の真似でもあるのと、doc.person.@id のようにはできなかったからなのです。先頭の @ は有効になるんだけど、TryGetMember メンバでだと、「id」としか取れないので、「@id」と「id」は同じ名前として扱われるためです。残念。

[code lang=”csharp”]
public void TestQuery()
{
var xml = @”
masuda tomoaki yumi
“;
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);
}
[/code]

まあ、それでも、連想配列は属性で、配列は子要素で示せるわけで、なかなかいい感じでパースができます。

子孫要素も含めて調べるときは「*」演算子のかわりに、タグ名に「_」を付けます。プロパティ名やメソッド名を後付けでできる(実行時に内部で分解して分岐させる)のが dynamic の良いところです。

[code lang=”csharp”]
public void TestSelectAttr()
{
var xml = @”


title
title


“;

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”]);
}
[/code]

これらをどう実装しているかというと、参照だけならば以下で ok です。

– System.Dynamic.DynamicObject を継承する
– TryGetMember を実装する
– TryGetIndex を実装する
– == 演算子を再定義する。

まあ、先行きは値の設定までやるので、TrySetMember や TrySetIndex も実装していきます。
具体的な説明は、.net core の DynamicObject のコードを読むか(コードに詳しい説明が書いてあります)、「メタプログラミング .NET」にざっと書いてあります。

[code lang=”csharp”]
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; } ///

/// 探索
///

/// /// ///
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();
}

///

/// 文字列にキャスト
///

/// public static implicit operator string(ExElement el)
{
return el.Value;
}
///

/// 数値にキャスト
///

/// public static implicit operator int(ExElement el)
{
return int.Parse(el.Value);
}
///

/// 実数にキャスト
///

/// public static implicit operator double(ExElement el)
{
return double.Parse(el.Value);
}
}
[/code]

もうちょっと整理して、前バージョンの ExDoc と同じように使えたら NuGet にアップしましょう。

カテゴリー: C#, EXDoc パーマリンク