C#基礎知識梳理系列十四:序列化


摘 要

說到序列化,大家都非常熟悉XML序列化,還有二進制序列化,經過序列化的數據流更方便傳輸和存儲。其實我們可以對序列化進行更多的控制,比如對序列化(和反序列化)前后的數據操作、定義自己的可序列化類型等。這一章我們來討論一下有關於序列化和反序列化。

第一節 序列化

1、 序列化

序列化包括正向序列化和反向序列化,一般我們將正向序列化說成是序列化。

序列化(Serialization)是將一個類對象轉化成一個字節流。

反序列化(Deserialization)是將一個字節流轉化成一個對應的類對象的過程。

在WCF通信中,當向服務端發送請求的時候,WCF是先把本地的內存對象序列化成XML或Binary通過信道傳送給服務端,服務端是把接收到序列化后的數據再將其轉化成對應的類對象,當服務端處理完畢向客戶發送數據時是一個恰恰相反的序列化/反序列化過程。

經過序列化后的流數據可以持久化,也可以跨平台傳送,比如WebService。

2、 C#提供的XML和二進制序列化

C#提供了兩種主要的序列化:XmlSerializer和BinaryFormatter,SoapFormatter已經不再使用。

XmlSerializer是XML序列化,是將對象序列化成XML及其反向的過程。

如下代碼是將一個Student對象序列化到一個XML文件:

        private void TestXmlSerializer()
        {
            Student student = new Student() { Name = "小明", Age = 15 };
            XmlSerializer xs = new XmlSerializer(typeof(Student));
            using (Stream stream = new FileStream("c:\\Student.xml", FileMode.Create, FileAccess.Write, FileShare.Read))
            {
                xs.Serialize(stream, student);
            }
        }

最終的XML文檔:

<?xml version="1.0"?>
<Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>小明</Name>
  <Age>15</Age>
</Student>

我們當然也可以從一個XML文檔反序列化到一個對象:

            using (FileStream fs = new FileStream("c:\\Student.xml", FileMode.Open, FileAccess.Read))
            {
                student = (Student)xs.Deserialize(fs);
            }

再來看一下BinaryFormatter:

        private void TestBinaryFormatter()
        {
            //
            Student student = new Student() { Name = "小明", Age = 15 };
            //序列化
            using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Create))
            {
                BinaryFormatter b = new BinaryFormatter();
                b.Serialize(fileStream, student);
            }
            //反序列化
            using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Open,FileAccess.Read))
            {
                BinaryFormatter bf = new BinaryFormatter();
                student = (Student)bf.Deserialize(fileStream);
            }
        }

無論是XML序列化還是二進制序列化,從以上示例可以看到,都是以流作為媒介,在得到一個對象的數據流后,我們可以對流進行傳送、加密等操作。

有一點要注意的是,不能使用XmlSerializer反序列化BinaryFormatter序列化過的流,也不能用BinaryFormatter反序列化XmlSerializer序列化過的流,雖然二者都是序列格式化器,但二者不能通用。

BinaryFormatter還支持將多個類型的對象序列化到一個流中,如下:

        private void TestBinaryFormatter()
        {
            //
            Teacher teacher = new Teacher() { Name = "王老師", Age = 45 };
            Student student = new Student() { Name = "小明", Age = 15 };
            //序列化
            using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Create))
            {
                BinaryFormatter b = new BinaryFormatter();
                b.Serialize(fileStream, teacher);
                b.Serialize(fileStream, student);
            }
            //反序列化
            using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Open, FileAccess.Read))
            {
                BinaryFormatter bf = new BinaryFormatter();
                teacher = (Teacher)bf.Deserialize(fileStream);
                student = (Student)bf.Deserialize(fileStream);
            }
        }

但是XmlSerializer不支持類似上面的操作,因為在構造XmlSerializer格式化器的時候已經明確了即將進行序列化的類型,如果想實現類似BinaryFormatter的操作,可以將多個對象包裝進一個對象中,然后再進行序列化,如下的一個教師學生類中包括了Teacher和Student:

    [Serializable]
    public class TeacherStudent
    {
        public Teacher Teacher { get; set; }
        public Student Student { get; set; }
    }
//看一下序列化/反序列化代碼:
            TeacherStudent ts = new TeacherStudent();
            ts.Teacher = new Teacher() { Name = "王老師", Age = 45 };
            ts.Student = new Student() { Name = "小明", Age = 15 };
            XmlSerializer xsts = new XmlSerializer(typeof(TeacherStudent));
            using (Stream stream = new FileStream("c:\\TeacherStudent.xml", FileMode.Create, FileAccess.Write, FileShare.Read))
            {
                xsts.Serialize(stream, ts);
            }
            using (FileStream fs = new FileStream("c:\\TeacherStudent.xml", FileMode.Open, FileAccess.Read))
            {
                ts = (TeacherStudent)xsts.Deserialize(fs);
            }

序列化/反序列化是基於反射實現的。

序列化過程中,格式化器首先調用類FormatterServices的一個靜態方法GetSerialization獲取所有公共和私有字段成員MemberInfo(未標記NonSerialized)數組,然后根據這個成員數組獲取其對應的值數組,接着向流中寫入程序集及類型信息,最后是遍歷以上兩個數組將對應的值寫入流中。

反序列化是序列化的一個相逆過程,格式化器先從流中讀取程序集及類型信息,然后加載相應的程序集,接着初始化對應的類型對象,再是構造該對象的成員數組,然后從流中獲取相應的值數組,最后是將從流中獲取的值數組與類型對象的成員數組匹配並為對象成員賦值。

 

第二節 控制類型可否序列化

在序列化一個對象的時候,可能想明確要求某個類型可被序列化,也可能不希望某些字段被序列化,我們可以附加類型或字段的特性System. SerializableAttribute來達到要求。

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Delegate, Inherited = false)]
    public sealed class SerializableAttribute : Attribute
    {
        public SerializableAttribute();
    }

如上面的類TeacherStudent前就使用了特性[Serializable]。SerializableAttribute特性只能應用於類型、值類型、枚舉和委托類型,該特性是不能被繼承的,如下兩個類型:

    [Serializable]
    public class A
    { }
    public class B : A
    { }

類B是獲取不到類A的可序列化特性的,如果要使類型B可序列化,必須對其使用特性Serializable。

對於XmlSerializer序列化,默認即使不使用特性Serializable,也是可以對對象進行序列化的,則BinaryFormatter不然。在創建WCF應用時,WCF強制要求所有傳輸的對象必須使用數據契約,這個數據契約是WCF特有的一種面向服務的格式化器:DataContract和DataMember,有關這一部分,可參考MSDN文檔

特性Serializable用於類型時,該類型的所有字段都會被序列化,當我們希望某個字段不可被序列化時,可以使用特性System. NonSerializedAttribute:

    [AttributeUsage(AttributeTargets.Field, Inherited = false)]
    public sealed class NonSerializedAttribute : Attribute
    {
        public NonSerializedAttribute();
    }
//它只能用於字段。如下代碼:
    [Serializable]
    public class TeacherStudent
    {
        public Teacher Teacher { get; set; }
        public Student Student { get; set; }
        [NonSerialized]
        private int RootID;
    }

則字段RootID就不會被序列化。

另外,有時候由於版本或是遠程服務調用的問題,可能會出現類型成員與序列化后的數據流不一致的現象,比如類型中的字段比序列化后的數據流多了一個,這時如果強制反序列化,可能會出異常,因為在流數據中未能找到與對象對應的某一字段,為了解決這個問題,可以將新添加的字段附加特性OptionalField。如下我們為Student類型增加一個年級的字段:

        [OptionalField]
        private int Grade;

 

第三節 控制序列化/反序列化前后的數據

有時候,我們想對序列化前后的數據進行更多的控制,比如在序列化前對對象附加數據,把序列化后的流加密,反序列化之后對對象的數據進行修正等等,這些都是可以做到的。在可序列化類型中定義相應的方法並對方法使用特性,然后在序列化/反序列化時,格式化器會檢查類型中是否有相應的方法定義,如果有,則調用此方法。對於序列化/反序列化,通常有以下四個相應特性及方法:

(1) OnSerializing 序列化之前

        [OnSerializing]
        private void OnSerializing(StreamingContext context)
        {

        }
//格式化器在序列化開始之前調用此方法。

(2) OnSerialized 序列化之后

        [OnSerialized]
        private void OnSerialized(StreamingContext context)
        {

        }
//格式化器在序列化后調用此方法。

(3) OnDeserializing 反序列化之前

        [OnDeserializing]
        private void OnDeserializing(StreamingContext context)
        {

        }
//格式化器在反序列化開始之前調用此方法。

(4) OnDeserialized 反序列化之后

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {

        }
//格式化器在反序列化開始之后調用此方法。

如下代碼,我們對Student類型,在序列化前判斷,如果年齡小於7周歲,則為它賦默認值為7,在反序列化之后也進行相應的操作(當然,這里可以使用屬性的來達到相同的目的,但我們這里只是演示如何控制序列化和反序列化前后的數據)。

    [Serializable]
    public class Student
    {

        public string Name { get; set; }
        public int Age { get; set; }

        [OnSerializing]
        private void OnSerializing(StreamingContext context)
        {
            if (this.Age<7)
            {
                this.Age = 7;
            }
        }

        [OnSerialized]
        private void OnSerialized(StreamingContext context)
        {

        }
        [OnDeserializing]
        private void OnDeserializing(StreamingContext context)
        {

        }
        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            if (this.Age < 7)
            {
                this.Age = 7;
            }
        }

    }

注意:以上的控制方法只對BinaryFormatter和SoapFormatter有效,對XmlSerializer無效。四個方法名可以是任意的,只是必須接收參數StreamingContext。StreamingContext是一個流上下文,它有兩個只讀屬性:

State 獲取傳輸數據的源或目標。

Context 獲取指定為附加上下文一部分的上下文信息。

 

第四節 自定義可序列化的類型

通過前面的討論,我們知道可以使用序列化/反序列化前后對應的四個方法來控制可序列化的數據,但是由於序列化是基於反射實現,它對性能有一定的損傷,並且這四個方法只能提供有限的操作。實現System.Runtime.Serialization. ISerializable接口的類型,不但可以相應提高性能,也提供了對序列化更豐富的操作。

    public interface ISerializable
    {
        [SecurityCritical]
        void GetObjectData(SerializationInfo info, StreamingContext context);
    }

ISerializable接口只有一個方法,該方法負責決定使用哪些信息來序列化對象,並調用SerializationInfo的AddValue方法將這些信息添加到SerializationInfo對象中。在序列化過程中,格式化器如果發現類型實現了接口ISerializable,則格式化器會調用GetObjectData方法。

下面我們創建一個實現了ISerializable接口的類People:

    [Serializable]
    public class People : ISerializable
    {
        string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        int _age;
        public int Age
        {
            get { return _age; }
            set { _age = value; }
        }
        public People()
        { }

        [SecurityCritical]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Name", _name);
            info.AddValue("Age", _age);
        }
    }

我們再來看一下對反序列化支持。在反序列化的過程中,格式化器如果發現類型實現了ISerializable接口,則格式化器會嘗試調用該類型的一個特殊的構造器,這個構造器的參數列表必須與GetObjectData的完全相同,通常是將該構造器聲明為私有或是受保護的。在構造器內會從SerializationInfo參數中讀取類型對應的字段數據(調用SerializationInfo的GetXXX方法)然后賦值給當前對象相應的字段。此處調用的GetXXX方法必須與其AddValue方法對應的數據類型一致,如People的構造器:

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected People(SerializationInfo info, StreamingContext context)
        {
            _name = info.GetString("Name");
            _age = info.GetInt32("Age");
        }

添加string類型的Name:info.AddValue("Name", _name);

必須調用獲取string類型的方法:_name = info.GetString("Name");

並且都是操作相同的屬性(字段)名。

使用實現了接口ISerializable的類型與之前無差別,如下:

                //序列化
                using (FileStream fileStream = new FileStream(string.Format("c:\\Serializer1\\People{0}.dat", i), FileMode.Create))
                {
                    BinaryFormatter b = new BinaryFormatter();
                    b.Serialize(fileStream, people);
                }
                //反序列化
                using (FileStream fileStream = new FileStream(string.Format("c:\\Serializer1\\People{0}.dat", i), FileMode.Open, FileAccess.Read))
                {
                    BinaryFormatter bf = new BinaryFormatter();
                    People p = null;
                    p = (People)bf.Deserialize(fileStream);
                }

注意,如果一個類型繼承了實現接口ISerializable的類型,那么在派生類的GetObjectData方法中必須先調用基類的方法:base.GetObjectData(info, context)。

 

小 結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM