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

「メタプログラミング .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()&#91;i&#93;);
                }
            }
            else if (indexes&#91;0&#93; is string)
            {
                string s = indexes&#91;0&#93; 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 にアップしましょう。

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