PG日誌

受託系 PG が C# の事を書いています

C#でオブジェクトの内容をXMLへ書き出す、読み込む

オブジェクトの内容を、XmlSerializerを使ってXMLへ読み(デシリアライズ)書き(シリアライズ)する方法のまとめです。

単純な読み書き程度はクラスさえ用意すれば数行で読み書きができます。また、標準機能を使用を使用するので特別な外部ライブラリは必要ありません。すべて.NET Frameworkの標準の機能で読み書きできます。かなり古くからある機能なので、知っていれば簡単なXML読み書き(のような、あまり面白くない作業)からある程度解放されます。

プロジェクトの準備

XmlSerializer を使用するためにプロジェクトの参照設定に System.Runtime.Serializationを 追加します。

f:id:Takachan:20170622223710p:plain

f:id:Takachan:20170622223728p:plain

前提知識

XmlSerializer はXMLを読み書きする時に、オブジェクトに付与されている属性によって処理が変化します。とは言っても特に重要な指示は以下の4種類しかありません。

// ルートタグを指定。一番先頭のクラスに指定できる。
[XmlRoot(ElementName = "root")]

// 要素名の指定。プロパティに指定できる。
[XmlElement(ElementName = "elem")]

// 属性名の指定。これもプロパティに指定できる。
[XmlAttribute(AttributeName = "att")]

// XMLへ読み書きしない指定。プロパティに指定可能。
[XmlIgnore]

省略された場合、プロパティ名が要素名として扱われ読み書きの対象となります。

読み書きするXMLサンプル

今回は以下のXMLを読み書きしたいと思います。

<root>
  <sub att-1="att-11" att-2="att-21">
    <string>aaaa</string>
    <long>1000</long>
    <double>0.005</double>
  </sub>
  <sub att-1="att-12" att-2="att-22">
    <string>bbbb</string>
    <long>5000</long><!-- 整数の読み書き -->
    <double>3.1415</double><!-- 小数点の読み書き -->
  </sub>
  <list>
     <!-- 属性と要素値の同時指定はできない -->
     <!--<member no="1">mem1</member>-->
     <member no="2">
        <value>val2</value>
     </member>
     <member no="3">
        <value>val3</value>
     </member>
     <member no="4">
        <value>val4</value>
     </member>
     <member no="5">
        <value>val5</value>
     </member>
  </list>
</root>

用意するクラス

サンプルXMLを読み取るために、XML構造に対応したクラスを用意し、メンバーに冒頭で紹介した属性を付与していきます。

[XmlRoot(ElementName = "root")]
public class Root
{
    [XmlElement(ElementName = "sub")]
    public List<Sub> SubList { get; set; } // 複数要素の場合リスト

    [XmlElement(ElementName = "list")]
    public MemberList MemberList { get; set; }
}
public class Sub
{
    // 属性の定義
    [XmlAttribute(AttributeName = "att-1")]
    public string FirstAttributte { get; set; }

    [XmlAttribute(AttributeName = "att-2")]
    public string SecondAttribute { get; set; }

    // 子要素の定義
    [XmlElement(ElementName = "string")]
    public string StringNode { get; set; }

    [XmlElement(ElementName = "long")]
    public int LongNode { get; set; } // 整数として読み取り

    [XmlElement(ElementName = "double")]
    public double DoubleNode { get; set; } // 浮動小数点として読み取り
}
public class MemberList
{
    [XmlElement(ElementName = "member")]
    public List<Member> Members { get; set; } // 複数要素の場合リスト
}

public class Member
{
    [XmlAttribute(AttributeName = "no")]
    public int No { get; set; }

    [XmlElement(ElementName = "value")]
    public string Value { get; set; }
}

読み書きするためのクラス

ファイルへの読み書きは多少コードを書かないといけないので共通化して以下のクラスとします。

public static class MyXmlSerializer
{
    // ファイルに書き出すときに使う
    public static void Serialize<T>(string savePath, T graph)
    {
        using (var sw = new StreamWriter(savePath, false, Encoding.UTF8))
        {
            var ns = new XmlSerializerNamespaces();
            ns.Add(string.Empty, string.Empty);

            new XmlSerializer(typeof(T)).Serialize(sw, graph, ns);
        }
    }

    // ファイルを読み取るときに使う
    public static T Deserialize<T>(string loadPath)
    {
        using (var sr = new StreamReader(loadPath))
        {
            return (T)new XmlSerializer(typeof(T)).Deserialize(sr);
        }
    }
}

使い方

読み込むXMLを d:\xmlser.xml に配置した時の使い方です。

internal static void Main(string[] args)
{
    // 読み取る
    Root deserializedObect = MyXmlSerializer.Deserialize<Root>(@"d:\xmlser.xml");

    // 書き込む
    MyXmlSerializer.Serialize(@"d:\xmlser_2.xml", deserializedObect);
}

まとめ

クラスさえ用意すれば、最終的に、1行で読み書きできるようになります。整数や小数、bool型は自動で解釈してオブジェクトに入れてくれます。オブジェクトとXMLの順序が違っても読み込んでくれます。

また、読み込んだファイルをそのまま別のファイルへ書き出して差分を取って全て一致していればテスト確認が即座に完了する点や、読み書きのロジックを個別に実装しないで済む点がかなりすぐれていると思います。

但し、

属性と要素値の同時読み込みはできない

制限が回避不可能なので、既存のXMLを読み書きする場合、構造によって使用できない局面が発生しそうです。また、自動で型を解釈してしまう機能が余計に感じるケースがあるかもしれません。

また使用していて思ったのですが、DateTime型に変換する場合が結構あってそういった場合は、以下のように設定するとよさそうです。(もっとほかの属性で指定できるのかもしれませんが)

[XmlElement(ElementName = "date")]
public string Date { get; set; }

[XmlIgnore]
public DateTime
{
    get
    {
        return DateTime.ParseExact(this.Date, "yyyyMMdd", ....);
    }
}