DataContractSerializerクラス

もう一つのXML形式シリアライザ

XML形式でのシリアライズについてXmlSerializerクラスを説明しましたが、DataContractSerializerというシリアライザを使用することもできます。

XmlSerializerクラスは以下のような制限があります。

  • シリアライズ対象のクラスはpublicな領域に定義し、クラス自体もpublicで定義しなければならない
  • publicメンバ(フィールド、プロパティ)しかシリアライズ対象にできない
  • デフォルトコンストラクター(引数なしコンストラクター)が必要
  • Dictionaryクラス、Hashtableクラスのシリアライズはできない

DataContractSerializerクラスはこれらの制限をクリアできます。
特にDictionaryクラスやHashtableクラスをシリアライズ対象に含めたい場合はDataContractSerializerクラスを使用するのが簡単です。
ただし、XmlSerializerクラスでは出力されるXMLをわりと細かく制御できましたが、DataContractSerializerクラスはそこまで細かい指定はできません。

DataContractSerializerクラスはSystem.Runtime.Serialization名前空間にあるので、usingディレクティブに追加しておきます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//↓を追加
using System.Runtime.Serialization;
using System.IO;

DataContractSerializerクラスは、シリアライズ対象のクラスがpublicな空間にあり、引数なしコンストラクタを持つ場合、そのpublicなメンバ(フィールド、プロパティ)をシリアライズすることができます。
この場合、復元時には引数なしコンストラクタが呼ばれます。
(XmlSerializerクラスと同じ動作)

publicメンバにIgnoreDataMemberAttribute属性をつけるとシリアライズの対象から除かれます。

引数なしコンストラクタを持たないクラスをシリアライズするには、クラスにDataContractAttribute属性を適用します。
そして、シリアライズに含めるメンバにDataMemberAttribute属性を適用します。
DataMemberAttribute属性を適用しない場合、publicメンバであってもシリアライズ対象になりません。

今回は以下のクラスをシリアライズしてみます。


class Program
{
    [DataContract]
    public class SimpleClass
    {
        [DataMember]
        public int Num;
        [DataMember]
        private string Str; //private

        //引数なしコンストラクタを持たないクラス
        public SimpleClass(int n, string s)
        {
            Num = n;
            Str = s;
        }

        //確認用
        public void Print()
        {
            Console.WriteLine(Num);
            Console.WriteLine(Str);
        }
    }

    static void Main(string[] args)
    {
    }
}

シリアライズ

実際にシリアライズするには、まずDataContractSerializerクラスのインスタンスを生成します。
コンストラクタにはtypeof演算子を使用して目的のクラスのTypeオブジェクトを指定します。
このDataContractSerializerインスタンスはコンストラクタに指定したクラス専用のシリアライザとなります。

シリアライズにはWriteObjectメソッドを使用します。
このメソッドはオーバーロードがいくつかありますが、まずは最も簡単なStream派生クラスを使用する方法を説明します。
第一引数はStream派生クラスのインスタンス、第二引数はシリアライズする実際のオブジェクト(クラスインスタンス)を指定します。


static void Main(string[] args)
{
    SimpleClass sc = new SimpleClass(1, "abc");

    using (FileStream fs = new FileStream("test.xml", FileMode.Create))
    {
        DataContractSerializer ds =
                new DataContractSerializer(typeof(SimpleClass));
        ds.WriteObject(fs, sc);
    }
}

出力されるXMLは以下のようになります。


<Program.SimpleClass xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Num>1</Num>
    <Str>abc</Str>
</Program.SimpleClass>

このサイト上では、見やすいようにXMLデータに改行とインデント(字下げ、タブ文字)を挿入していますが、実際は改行もインデントもなしの一行で出力されます。
データ量が多くなると人が直接読み書きするには不便なので、そのような用途にはXmlSerializerクラスを使用するか、後述するXmlWriterクラスを併用したほうが良いです。

これ以降のXML出力データは、特に説明がない限り当サイト側で改行やインデントを挿入しています。
その場合、実際に出力されるのは一行のデータです。

デシリアライズ

シリアライズされたXMLをデシリアライズ(復元)するにはReadObjectメソッドを使用します。


using (FileStream fs = new FileStream("test.xml", FileMode.Open))
{
    DataContractSerializer ds =
            new DataContractSerializer(typeof(SimpleClass));
    SimpleClass sc2 = (SimpleClass)ds.ReadObject(fs);

    sc2.Print();
}
1
abc

引数は対象のXMLデータを読み取るStream派生クラスを指定します。
戻り値はデシリアライズされたクラスのインスタンスですが、object型で返されるのでキャストが必要です。

コンストラクタは呼ばれない

DataContractAttribute属性が付いているクラスに対しては、DataContractSerializerクラスは復元時にコンストラクタを呼び出しません。
復元するデータ内に値が存在しないメンバは既定値で初期化されます。


[DataContract]
public class SimpleClass
{
    [DataMember]
    public int Num;

    //DataMember属性のないフィールドは
    //シリアライズの対象にならない
    public string Str;

    //引数なしコンストラクタ
    public SimpleClass() 
    {
        Num = 99;
        Str = "xyz";
    }
    public SimpleClass(int n, string s)
    {
        Num = n;
        Str = s;
    }
}

static void Main(string[] args)
{
    SimpleClass sc = new SimpleClass(1, "abc");

    using (FileStream fs = new FileStream("test.xml", FileMode.Create))
    {
        DataContractSerializer ds =
                new DataContractSerializer(typeof(SimpleClass));
        ds.WriteObject(fs, sc);
    }

    using (FileStream fs = new FileStream("test.xml", FileMode.Open))
    {
        DataContractSerializer ds =
                new DataContractSerializer(typeof(SimpleClass));
        SimpleClass sc2 = (SimpleClass)ds.ReadObject(fs);

        Console.WriteLine(sc2.Num);
        Console.WriteLine(sc2.Str == null ? "(null)" : sc2.Str);
    }
}
1
(null)

このクラスのフィールドStrは、通常であれば引数なしコンストラクタもしくは引数ありコンストラクタで指定した値で初期化されます。
しかしDataContractSerializerクラスはコンストラクタを呼ばないので、DataMemberAttribute属性のないフィールドStrはシリアライズの対象から外れ、デシリアライズ時にはXMLにデータがないため既定値であるnullで初期化されます。

名前空間とクラス構造の指定(省略)

先ほど作成したXMLデータには、C#の名前空間とクラスの構造が保存されています。
(見やすいようにタグの属性の手前に改行を挿入しています)


<Program.SimpleClass
    xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1"
    xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Num>1</Num>
    <Str>abc</Str>
</Program.SimpleClass>

タグ名のProgram.SimpleClassがC#コード上のクラスの構造です。
(Programクラス内のSimpleClassクラス)
xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1"ConsoleApplication1がC#名前空間です。

つまり以下のような構造が保存されています。


namespace ConsoleApplication1
{
    class Program
    {
        public class SimpleClass {}
    }
}

このXMLデータは、同じ名前空間、同じ構造で書かれたコード以外からはDataContractSerializerクラスで復元することができません。
コードの変更や他アプリケーションとのデータのやり取りには不都合なので、これらの情報は出力しないようにしておいたほうが良いでしょう。

名前空間とタグ名を変更するには、DataContractAttribute属性の名前付き引数を使用します。


    [DataContract(Namespace = "", Name = "simpleClass")]
    public class SimpleClass
    {
        [DataMember]
        public int Num;
        [DataMember]
        public string Str;
    }

Namespaceが名前空間の指定、Nameがタグ名の指定です。
名前空間には空文字を指定しているので、名前空間は読み書きから無視されます。

このクラスは以下のようなXMLを出力します。


<simpleClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Num>1</Num>
    <Str>abc</Str>
</simpleClass>

クラス名は「SimpleClass」ですが、タグ名は「simpleClass」と先頭が小文字になっていることに注目してください。
名前付き引数Nameを指定すると、クラス名には依存せずここで指定した名前のXMLタグを読み書きするようになります。

また、XML名前空間(「xmlns:i=~」というやつ)が出力されていますが、復元時はこれがなくても正常に読み取り可能です。
つまり以下のようなごくシンプルなXMLをデシリアライズ可能になります。


<simpleClass>
    <Num>1</Num>
    <Str>abc</Str>
</simpleClass>

XMLタグの並び順とタグ名

DataContractSerializerクラスは、対象のクラス内の(シリアライズ対象の)メンバを名前順(アルファベット順)で出力します。
復元時もそれに従いデータを復元しようとします。


[DataContract(Namespace = "", Name = "simpleClass")]
public class SimpleClass
{
    [DataMember]
    public int Number;
    [DataMember]
    public string Name;
}

このクラスは以下のXMLを出力します。


<simpleClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Name>John</Name>
    <Number>1</Number>
</simpleClass>

改行やインデントはサイト側で挿入していますが、これらは入っていても復元可能です。
しかしXMLを編集してタグの順序を変えてしまうと正常に復元できなくなります。


<simpleClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Number>1</Number>
    <Name>John</Name>
</simpleClass>

この場合、先にNumberタグが読み取られるため、それよりも前のメンバ(名前順で先になるメンバ)は存在しないものとされます。
そのためNameメンバは復元できずに既定値で初期化されます。

なお、XMLタグ名と並び順はDataMemberAttribute属性の名前付き引数で変更することができます。


[DataContract(Namespace = "", Name = "simpleClass")]
public class SimpleClass
{
    [DataMember(Name = "number", Order = 0)]
    public int Number;
    [DataMember(Name = "name", Order = 1)]
    public string Name;
}

<simpleClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <number>1</number>
    <name>John</name>
</simpleClass>

引数Nameがタグ名、引数Orderが順序の指定です。
順序は数値が小さいほど先になります。

順序を属性で指定しても、読み取るXMLデータの順序が外部で変更されると復元できなくなるのは変わらないので注意してください。
XmlSerializerクラスは順序に関係なく復元可能なので、順序が変わってしまう可能性がある場合はこちらを使用したほうが良いでしょう。

出力されるXMLタグの並び順は正確には以下のようになります。

  1. クラスが継承されている場合、継承元のクラスのメンバ
  2. Orderが指定されていないメンバ(名前順)
  3. Orderが指定されているメンバ。Orderの値が同じ場合は名前順

なお、XmlSerializerクラスは順序に関係なく復元できると書きましたが、クラスが継承されている場合は継承元のメンバが先に記述されていないと復元できません。

XmlWriterとXmlReader

先ほどはシリアライズ/デシリアライズ時にStream派生クラス(FileStreamクラス)を使用していましたが、XmlWriterクラスとXmlReaderクラスを使用することもできます。


[DataContract(Namespace = "", Name = "simpleClass")]
public class SimpleClass
{
    [DataMember]
    public int Num;
    [DataMember]
    public string Str;

    public SimpleClass(int n, string s)
    {
        Num = n;
        Str = s;
    }

    public void Print()
    {
        Console.WriteLine(Num);
        Console.WriteLine(Str);
    }
}

static void Main(string[] args)
{
    SimpleClass sc = new SimpleClass(1, "abc");

    XmlWriterSettings xws = new XmlWriterSettings();
    xws.Encoding = new System.Text.UTF8Encoding(false);
    xws.Indent = true;

    using(XmlWriter xw = XmlWriter.Create("test.xml", xws))
    {
        DataContractSerializer ds =
            new DataContractSerializer(typeof(SimpleClass));
        ds.WriteObject(xw, sc);
    }

    using (XmlReader xr = XmlReader.Create("test.xml"))
    {
        DataContractSerializer ds =
            new DataContractSerializer(typeof(SimpleClass));
        SimpleClass sc2 = (SimpleClass)ds.ReadObject(xr);
        sc2.Print();
    }
}
1
abc

出力されるXMLは以下のようになります。


<?xml version="1.0" encoding="utf-8"?>
<simpleClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Num>1</Num>
  <Str>abc</Str>
</simpleClass>

XmlWriterSettingsクラス

XmlWriterクラスはXmlWriterSettingsクラスと併用します。
XmlWriterSettingsクラスは名前の通り、XML書き込み時の設定を保存するクラスです。

Encodingプロパティは文字エンコーディングの指定です。
デフォルトはUTF-8、BOM付きで、今回はBOMなしのUTF-8を指定しています。
IndentプロパティはXMLデータにインデントと改行を挿入します。
デフォルトはfalseなので、指定しない場合は一行のXMLが出力されます。

出力されたXMLを見ると、先頭にXML宣言が挿入されています。
(<?xml version="1.0" encoding="utf-8"?>というやつ)
XML宣言はなくても問題はありませんが、文字エンコーディングにUTF-8またはUTF-16以外を使用している場合は指定しなければならないそうです。
XML宣言は、XmlWriterSettingsクラスのOmitXmlDeclarationプロパティにfalseを指定すると出力されなくなります。

文字エンコーディングについてはEncodingクラスStreamReader/Writerクラス#BOMなしで書き込むを参照してください)

シリアライズとデシリアライズ

XmlWriterクラスはXmlWriter.Create静的メソッドでインスタンスを生成します。
第一引数は出力するファイルパス、第二引数は先ほどのXmlWriterSettingsクラスのインスタンスを指定します。

シリアライズを行うDataContractSerializerクラスのWriteObjectメソッドの第一引数にはXmlWriterクラスのインスタンスを指定します。
第二引数はシリアライズするクラスのインスタンスを指定します。

デシリアライズのほうはStream派生クラスの時とほぼ同じで、XmlReader.Create静的メソッドでXmlReaderクラスのインスタンスを指定し、ReadObjectメソッドの引数に指定します。

これらはFileStreamと同じくusingステートメントでオブジェクト(インスタンス)を自動的に破棄でき、Closeメソッドで明示的に破棄することもできます。

XmlWriter.Create静的メソッドの第一引数はStream派生クラスを指定することもできます。
この場合、文字エンコーディングはそのStream派生クラスのものが使用され、XmlWriterSettingsクラスのEncodingプロパティは無視されます。

XmlSerializerクラスでの使用

XmlWriterクラスはXmlSerializerクラスによるシリアライズにも使用することができます。
XmlWriterSettingsクラスによるカスタマイズも有効なので、改行を無くしたりXML宣言を省いたりすることができます。


[DataContract(Namespace = "", Name = "simpleClass")]
public class SimpleClass
{
    [DataMember(Name = "number", Order = 0)]
    public int Number;
    [DataMember(Name = "name", Order = 1)]
    public string Name;

    public SimpleClass() {} //XmlSerializerは引数なしコンストラクタが必要
    public SimpleClass(int n, string s)
    {
        Number = n;
        Name = s;
    }

    public void Print()
    {
        Console.WriteLine(Number);
        Console.WriteLine(Name);
    }
}

static void Main(string[] args)
{
    SimpleClass sc = new SimpleClass(1, "abc");

    XmlWriterSettings xws = new XmlWriterSettings();
    xws.Encoding = new System.Text.UTF8Encoding(false);
    xws.OmitXmlDeclaration = true;
    xws.Indent = true;
    xws.NewLineOnAttributes = true;

    using (XmlWriter xw = XmlWriter.Create("test.xml", xws))
    {
        XmlSerializer xs =
            new XmlSerializer(typeof(SimpleClass));
        xs.Serialize(xw, sc);
        xw.Dispose();
    }

    using (XmlReader xr = XmlReader.Create("test.xml"))
    {
        XmlSerializer xs =
            new XmlSerializer(typeof(SimpleClass));
        SimpleClass sc2 = (SimpleClass)xs.Deserialize(xr);
        sc2.Print();
    }
}
1
abc

XMLは以下のようになります。


<SimpleClass
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Num>1</Num>
  <Str>abc</Str>
</SimpleClass>

クラスにDataContractAttribute属性やDataMemberAttribute属性が付いていますが、これらはDataContractSerializerクラスで使用される属性なのでXmlSerializerクラスでは無視されます。

XmlWriterSettingsクラスの設定は、OmitXmlDeclarationプロパティにtrueを指定してXML宣言の出力を省略しています。
また、NewLineOnAttributesというプロパティにtrueを指定していますが、これはタグの属性の手前に改行を挿入する設定です。
この設定はIndentプロパティがfalseの場合は無視されます。