2011年7月24日日曜日

.NET Framework XmlSerializerでコレクションを読み書きするときの注意

XmlSerializerでコレクションを持ったクラスをDeserializeしたときにちょっとだけハマったのでメモ。 なんか、プログラムを実行するたびにシリアライズで出力したxmlファイルが大きくなるんですよ。 xmlファイルを見たら、コレクションの中に同じ内容がいくつも追加されていました。 「これはどういうことだ?」と次のようなテストコードを書いてみました。

using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System;

namespace XmlSerializerTest
{
    public class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Program p = new Program();
                Program.Save(p);
                p.Print();
                p = Program.Load();
                p.Print();
            }
            catch{}
        }

        public List<int> TestList { get; set; }

        public Program()
        {
            TestList = new List<int>();
            TestList.Add(0);
            TestList.Add(1);
            TestList.Add(2);
        }

        public static void Save(Program instance)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Program));
            FileStream stream = new FileStream("test.xml", FileMode.Create);
            serializer.Serialize(stream, instance);
            stream.Close();
        }

        public static Program Load()
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Program));
            FileStream stream = new FileStream("test.xml", FileMode.Open);
            Program res = (Program)serializer.Deserialize(stream);
            stream.Close();
            return res;
        }

        public void Print()
        {
            Console.WriteLine("==== Print start ====");
            foreach (int i in TestList)
            {
                Console.WriteLine("  " + i);
            }
            Console.WriteLine("====  Print end  ====\n\n");
        }
    }
}

例外処理は手抜きです。 コンソールアプリケーションなのでコマンドプロンプトで実行します。 実行結果は、

C:\~\XmlSerializerTest\bin\Debug>XmlSerializerTest.exe
==== Print start ====
  0
  1
  2
====  Print end  ====


==== Print start ====
  0
  1
  2
  0
  1
  2
====  Print end  ====

セーブした直後は最初に登録した0、1、2のみの表示。 Deserializeした後はダブって登録されてます。

もうちょっと調べるためにWriteLineを追加してみました。

using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System;

namespace XmlSerializerTest
{
    public class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Program p = new Program();
                Program.Save(p);
                p.Print();
                p = Program.Load();
                p.Print();
            }
            catch
            {}
        }

        private List<int> _TestList;
        public List<int> TestList
        {
            get { return _TestList; }
            set
            {
                Console.WriteLine("List set. value.Count = " + value.Count);
                _TestList = value;
            }
        }

        public Program()
        {
            TestList = new List<int>();
            TestList.Add(0);
            TestList.Add(1);
            TestList.Add(2);
            Console.WriteLine("※Constructor. list.Count = " + TestList.Count);
        }

        public static void Save(Program instance)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Program));
            FileStream stream = new FileStream("test.xml", FileMode.Create);
            serializer.Serialize(stream, instance);
            stream.Close();
        }

        public static Program Load()
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Program));
            FileStream stream = new FileStream("test.xml", FileMode.Open);

            Console.WriteLine("before Deserialize");
            Program res = (Program)serializer.Deserialize(stream);
            Console.WriteLine("after Deserialize");

            stream.Close();
            return res;
        }

        public void Print()
        {
            Console.WriteLine("==== Print start ====");
            foreach (int i in TestList)
            {
                Console.WriteLine("  " + i);
            }
            Console.WriteLine("====  Print end  ====\n\n");
        }
    }
}

結果は、

C:\~\XmlSerializerTest\bin\Debug>XmlSerializerTest.exe
List set. value.Count = 0
※Constructor. list.Count = 3
==== Print start ====
  0
  1
  2
====  Print end  ====


before Deserialize
List set. value.Count = 0
※Constructor. list.Count = 3
after Deserialize
==== Print start ====
  0
  1
  2
  0
  1
  2
====  Print end  ====

注目すべきはDeserializeメソッドの前後です。 Programクラスのコンストラクタが呼ばれています。 そして、TestListプロパティのsetは1度しか呼ばれていません。 コンストラクタで作成されたListが使いまわされているのです。 そのため、

  1. コンストラクタで0、1、2を登録
  2. XMLファイルから読み込んだ0、1、2を追加で登録

と2度の登録がされているのです。 これを防ぐには、

  1. 引数無しのコンストラクタではコレクションクラスに初期値の登録をしない。
  2. Deserialize前にコレクションクラスの中身を全てクリアする。

のどちらかをする必要がありますね。