Effective C# Item25:儘可能將類型實現爲可序列化的類型

    持久化是類型的一個核心特性,有時我們需要通過不同的方式傳輸和創建同一個對象,例如需要通過網絡傳輸對象,或者需要將對象信息存儲到文本文件或者XML文件中,這時,如何能夠保持對象的狀態,並在將來使用時,可以準確的還原到原來的狀態,是非常重要的。

    .NET可以使用序列化的方式來持久化對象,我們在可能的情況下,應該將類型定義爲可以序列化的。序列化時,我們可以使用Serializable特性。

    我們來看下面的簡單示例,將對象信息存儲到XML文件中。

    首先定義兩個可以序列化的類型。

    [Serializable]
    public class Employee
    {
        private PersonName m_Name;
        public PersonName Name
        {
            get { return m_Name; }
            set { m_Name = value; }
        }

        private string m_strAddress;
        public string Address
        {
            get { return m_strAddress; }
            set { m_strAddress = value; }
        }

        public Employee()
        {
            m_Name = new PersonName();
            m_strAddress = string.Empty;
        }

        public Employee(PersonName name, string address)
        {
            m_Name = name;
            m_strAddress = address;
        }

        public override string ToString()
        {
            return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
        }
    }

    [Serializable]
    public class PersonName
    {
        private string m_strFirstName;
        public string FirstName
        {
            get { return m_strFirstName; }
            set { m_strFirstName = value; }
        }

        private string m_strLastName;
        public string LastName
        {
            get { return m_strLastName; }
            set { m_strLastName = value; }
        }

        public PersonName()
        {
            m_strFirstName = string.Empty;
            m_strLastName = string.Empty;
        }

        public PersonName(string firstName, string lastName)
        {
            m_strFirstName = firstName;
            m_strLastName = lastName;
        }
    }


    下面是測試方法,包含了序列化和反序列化的過程。

    private static void Test()
    {
        Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
        Console.WriteLine("Ouput emp info before serialize:");
        Console.WriteLine(emp.ToString());

        XmlSerializer serializer = new XmlSerializer(typeof(Employee));
        StreamWriter writer = new StreamWriter("emp.xml");
        serializer.Serialize(writer, emp);
        writer.Close();
        Console.WriteLine("Serialization Success.");

        StreamReader reader = new StreamReader("emp.xml");
        XmlSerializer desrializer = new XmlSerializer(typeof(Employee));
        object o = desrializer.Deserialize(reader);
        if (o is Employee)
        {
            Console.WriteLine("Ouput emp info after deserialize:");
            Console.WriteLine((o as Employee).ToString());
        }
    }


    上述代碼的執行結果如下所示。

    可以看到序列化前和序列化後的對象信息,被完全一致的反應出來,這說明,序列化確實實現了對象的持久化。

    有以下兩個問題需要注意:

  1. 對於可以序列化的類型,必須提供沒有參數的構造函數,上述代碼中,如果Employee類型和PersonName類型沒有顯示的提供默認構造函數,那麼程序在編譯時,就會報錯,提示在不提供沒有參數的構造函數的情況下,無法執行序列化操作。
  2. 出於性能的考慮,我們可以不用將類型全部內容都置爲可序列化。

    C#在序列化時,可以採用上面代碼中寫的XmlSerializer的方式,也可以採用BinaryFormatter的方式,如果採用XmlSerializer的方式,那麼NonSerialized特性是不會發揮作用的,同時這種方式允許序列化中的類型包含不能序列化的類型;但是對於BinaryFormatter來說,可以使用NonSerialized特性,同時進行序列化的類型所包含的其他所有類型,都必須是可序列化的,否則就會在序列化的過程中發生異常。

    我們來看以下的代碼,使用Formatter的正常方式。

        private static void TestWithFormatter()
        {
            Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
            Console.WriteLine("Ouput emp info before serialize:");
            Console.WriteLine(emp.ToString());

            BinaryFormatter serializer = new BinaryFormatter();
            Stream stream = File.Open("emp.txt", FileMode.Create);
            serializer.Serialize(stream, emp, null);
            stream.Close();
            Console.WriteLine("Serialization Success.");

            stream = File.Open("emp.txt", FileMode.Open); 
            BinaryFormatter deserializer = new BinaryFormatter();
            object o = deserializer.Deserialize(stream, null);
            stream.Close();
            if (o is Employee)
            {
                Console.WriteLine("Ouput emp info after deserialize:");
                Console.WriteLine((o as Employee).ToString());
            }
        }


    我們來修改一下Employee類型的代碼,將m_strAddress字段用NonSerialized特性進行修飾,然後執行上述Test()方法和TestWithFormatter()方法,其中,Test()方法的執行結果是不會改變的;但是TestWithFormatter()方法的執行結果如下所示。

    我們來修改一下Employee類型的代碼,將m_strAddress字段用NonSerialized特性進行修飾,然後執行上述Test()方法和TestWithFormatter()方法,其中,Test()方法的執行結果是不會改變的;但是TestWithFormatter()方法的執行結果如下所示。

    [Serializable]
    public class Employee : IDeserializationCallback
    {        
        private PersonName m_Name;
        public PersonName Name
        {
            get { return m_Name; }
            set { m_Name = value; }
        }

        [NonSerialized]
        private string m_strAddress;
        public string Address
        {
            get { return m_strAddress; }
            set { m_strAddress = value; }
        }

        public Employee()
        {
            m_Name = new PersonName();
            m_strAddress = string.Empty;
        }

        public Employee(PersonName name, string address)
        {
            m_Name = name;
            m_strAddress = address;
        }

        public override string ToString()
        {
            return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
        }

        public void OnDeserialization(object sender)
        {
            this.Address = "Vernus";
        }
    }


    上述代碼實現了IDeserializationCallBack接口,在接口的OnDeserialization()方法中,我們對Address屬性進行重新賦值,在代碼修改後,TestWithFormatter()方法的執行結果如下圖所示。

    我們在編碼的過程中,可能在將對象序列化後,又修改了類型的結構,這時,單純採用Serialzable特性,是不能對修改進行區分的,甚至可能會引發異常。這時,我們可以實現ISerializable接口,來定製序列化過程。

    我們來看下面的代碼。

    [Serializable]
    public class Employee : IDeserializationCallback, ISerializable
    {        
        private PersonName m_Name;
        public PersonName Name
        {
            get { return m_Name; }
            set { m_Name = value; }
        }

        [NonSerialized]
        private string m_strAddress;
        public string Address
        {
            get { return m_strAddress; }
            set { m_strAddress = value; }
        }

        public Employee()
        {
            m_Name = new PersonName();
            m_strAddress = string.Empty;
        }

        public Employee(PersonName name, string address)
        {
            m_Name = name;
            m_strAddress = address;
        }

        private Employee(SerializationInfo info, StreamingContext context)
        {
            Name = info.GetValue("name", typeof(PersonName)) as PersonName;
            Address = info.GetString("address");
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("name", Name);
            info.AddValue("address", Address);
        }

        public void OnDeserialization(object sender)
        {
            this.Address = "Vernus";
        }

        public override string ToString()
        {
            return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
        }
    }


    上述代碼在執行TestWithFormatter()方法後的結果沒有發生變化。我們在ISerializable接口的GetObjectData()方法中,以鍵/值對的方式將類型中的數據項進行存儲,同時添加了一個構造函數,用於得到反序列化後對象中各數據項的值。

    一般情況下,我們對實現了ISerializable接口的類聲明爲sealed,這樣可以免於被繼承,因爲繼承後的子類,還要針對自身的數據情況,對GetObjectData()方法進行擴充,比較複雜。

    編程時要注意,從序列化流中寫入和讀取數據的順序必須一致。

    總結:.NET框架爲對象序列化提供了一個簡單、標準的算法。如果我們的類型需要持久化,那就應該遵循標準的實現。如果我們不爲類型添加序列化支持,那麼其他使用我們類型的類也就不能支持序列化。我們所做的工作應該儘可能的使類型的使用者更加方便。如果可以,應該使用默認的方式來支持序列化;如果默認的Serializable特性不能夠滿足要求,則應該實現ISerializable接口。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章