2015年6月30日

XmlSerializer と CDATA セクション

XmlSerializerCDATA セクションを扱うのが面倒だったので、自分なりのまとめ。(ぐぐれば引っかかる内容。)
System.Xml.Serialization 名前空間には、なぜか CDATA セクションのためのカスタム属性が用意されてない・・・

CDATA セクションのみを含む要素

[XML]

<root>
  <text><![CDATA[<b>cdata</b>]]></text>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml1
{
    [XmlElement("text")]
    public XmlCDataSection Text { get; set; }
}

XmlCDataSection のプロパティを用意するだけ。
XmlCDataSection は new XmlDocument().CreateCDataSection("CDATA セクションの中身") で作成できる。
面倒な場合はプロパティで工夫。
※シリアライズ時、CreateCDataSection の引数が null でも <![CDATA[]]> は出力される。

XmlCDataSection の代わりに、IXmlSerializable を継承した CDATA セクション用クラスを自作するのもあり。
[CDATA セクション用クラス]

public class CDataSection : IXmlSerializable
{
    public CDataSection() { }
    public CDataSection(string text) { Text = text; }
    public string Text { get; set; }

    public virtual XmlSchema GetSchema()
    {
        return null;
    }
    public virtual void ReadXml(XmlReader reader)
    {
        Text = reader.ReadElementString();
    }
    public virtual void WriteXml(XmlWriter writer)
    {
        writer.WriteCData(Text);
    }
}

CDATA セクションと属性がある要素

[XML]

<root>
  <node attr="value"><![CDATA[<b>cdata</b>]]></node>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml3
{
    [XmlElement("node")]
    public CDataNode Node { get; set; }
}

public class CDataNode
{
    [XmlAttribute("attr")]
    public string Attr { get; set; }

    // データの設定・取得用
    [XmlIgnore]
    public string Content { get; set; }

    // シリアライズ・デシリアライズ用
    [XmlText]
    public XmlNode[] ContentNode
    {
        get { return new[] { new XmlDocument().CreateCDataSection(Content) }; }
        set
        {
            if( value == null ){
                Content = null;
                return;
            }
            if( value.Length != 1 ) throw new InvalidOperationException();
            Content = value[0].Value;
        }
    }
}

CDATA セクションにあたるプロパティを、XmlCDataSection を 1 つだけ含む XmlNode[] 型にして、XmlTextAttribute を付加。
そうすると、直接 CDATA セクションを出力できる。
XmlNode[] 型だとデータの設定・取得が面倒なので、別のプロパティ(上記だと Content)を用意する。

ただし、この方法はデシリアライズ時にちょっとした問題があり、CDATA セクションの代わりに普通のテキストノードでもデシリアライズできてしまう。
デシリアライズされたインスタンス(上記だと value[0])は、元が CDATA セクションかテキストノードかに関わらず XmlText になるため、型チェックではじくことができない。
OuterXmlInnerText などのプロパティにも <![CDATA[]]> の文字列が含まれないため、デシリアライズインスタンスからはおそらく判断不可能。

CDATA セクションかテキストノードかどちらでもいい場合は問題ないが、厳密にチェックしたい場合、この方法では難しいかもしれない。

参考URL

2015年6月20日

クラスと構造体のインターフェースメソッド呼び出し

IComparer<T>IEqualityComparer<T> などのインターフェースを実装する際、クラスと構造体のどちらがいいのか迷ったことがあったので、インターフェースにキャストしたクラス・構造体からの、メソッド呼び出し速度を測定してみた。

[測定コード]

static class MeasureMethodCall
{
    static void Main(string[] args)
    {
        var size = 10000000;
        var cmpc = new ComparerClass();  // クラスベース
        var cmps = new ComparerStruct(); // 構造体ベース

        for( var i=0; i<11; i++ ){
            Console.WriteLine("[{0}]", i);

            // インターフェースにキャストした状態からのメソッド呼び出し
            InterfaceCall(size, cmpc);
            InterfaceCall(size, cmps);

            // おまけ:直接のメソッド呼び出し
            DirectCall(size, cmpc);
            DirectCall(size, cmps);
        }
    }

    static void InterfaceCall(int size, IComparer<int> comparer)
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[InterfaceCall][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }

    static void DirectCall<T>(int size, T comparer) where T : IComparer<int>
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[DirectCall   ][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }
}

class ComparerClass : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}

struct ComparerStruct : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}
[結果]
インターフェース(クラス) 0.0758722
インターフェース(構造体) 0.1012821
クラス 0.0674410
構造体 0.0083828
※コマンドプロンプトで実施(csc /optimize)
※2 回目以降は類似の結果だったため、2 回目の結果を採択
※単位は[s]

まとめると、
構造体 ≫ クラス > インターフェース(クラス) > インターフェース(構造体)
※左の方が速い

当たり前だが、インターフェースにキャストした状態からのメソッド呼び出しは、直接呼び出しよりも遅い。
そして、インターフェースにキャストした状態では、クラスより構造体の方が遅い。
理由はたぶん、インターフェース(構造体)のメソッド呼び出しは、自身(構造体インスタンス)のボックス化解除が必要だから?

インターフェースを引数に取るメソッドが、上記のように where 制約で実装されていれば構造体がベストだが、.NET Framework の標準メソッドの多くはインタフェース引数に where 制約を使っていない。
where 制約を使うと、型引数が増えて呼び出し側での指定が必要になり、コードが汚くなるからだと思われる。(参考:メソッドの型推論で型パラメータの制約は使われない

結論としては、一般的な使い方をするなら、インターフェースはクラスで実装した方がいい。
※プロパティも実質メソッドなので、プロパティを実装するインターフェースについても同様

実際に Array.SortEnumerable.OrderBy で測定したところ、インターフェース(クラス)の方がいい結果になった。

検証環境

Windows 7 64bit/.NET 4.5.2
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

2015年6月5日

LINQ OrderByDescending != OrderBy + Reverse

LINQ でシーケンスを降順にソートする場合、素直に OrderByDescending を使う方法と、OrderByReverse を組み合わせる方法がある。
(降順ソート用 IComparer<T> を使う方法もあるが、OrderByDescending と同じなので割愛)

この 2 つの方法は、異なる結果になることがある。
理由は、OrderByDescending と OrderBy が両方とも 安定ソートであるため。
「降順の安定ソートってどっち?」と混乱するかもしれないが、OrderByDescending の実装は MSDN の記載通り同じキーを持つ要素の順序は保持される。
OrderBy も同様なので、同じキーを持つ要素については、OrderByDescending と OrderBy は同じ順序になってしまう。

つまり、両者は正反対の順序にならないため、OrderBy + Reverse は必ずしも OrderByDescending の結果と一致しない。
※並び替えキーに重複がない場合は一致する。
※要素=並び替えキーとなる場合は、順序が違っても分からないため、考慮不要

[不一致ケース]

// インデックス付き配列を並べ替え
var data = new[]{ 'a', 'b', 'a', 'c', 'a' }.Select((V, I) => new{ V, I });

Console.WriteLine("[OrderBy]");
Console.WriteLine(string.Join(" ", data.OrderBy(v => v.V)));

Console.WriteLine("[OrderBy + Reverse]");
Console.WriteLine(string.Join(" ", data.OrderBy(v => v.V).Reverse()));

Console.WriteLine("[OrderByDescending]");
Console.WriteLine(string.Join(" ", data.OrderByDescending(v => v.V)));
[結果]
[OrderBy]
{ V = a, I = 0 } { V = a, I = 2 } { V = a, I = 4 } { V = b, I = 1 } { V = c, I = 3 }
[OrderBy + Reverse]
{ V = c, I = 3 } { V = b, I = 1 } { V = a, I = 4 } { V = a, I = 2 } { V = a, I = 0 }
[OrderByDescending]
{ V = c, I = 3 } { V = b, I = 1 } { V = a, I = 0 } { V = a, I = 2 } { V = a, I = 4 }