C#之你懂得的序列化/反序列化

前言:寫此文章一方面是爲了鞏固對序列化的認識,另外一方面是由於本人最近在面試,面試中被問到「爲何要序列化」。雖然一直在使用,本身也反覆的提到序列化,可至於說爲何要序列化,還真的沒想過,因此本文就這樣產生了。面試

序列化是將一個對象轉換成一個字節流的過程。反序列化是將一個字節流轉換回對象的過程。在對象和字節流之間轉換是頗有用的一個機制。(固然這個還不能回答它的實際用處)數據庫

舉點例子:數組

  • 應用程序的狀態能夠保存到一個磁盤文件或數據庫中,並在應用程序下次運行時恢復。好比ASP.NET就是利用系列化和反序列化保存和恢復回話狀態。
  • 一組對象能夠輕鬆複製到系統的剪切板,而後再粘貼到其餘的地方(應用程序)。
  • 一組對象可克隆並放到其餘地方做爲備份。
  • 一組對象能夠經過網絡發送給另外一臺機器上運行的進程(好比Remoting)。

 除了上述的幾個場景,咱們能夠將系列化獲得的字節流進行任意的操做。網絡

 1、序列化、反序列化快速實踐ide

    [Serializable]
    class MyClass
    {
        public string Name { get; set; }
    }

一個自定義類,切記須要加上[Serializable]特性(可應用於class、struct、enum、delegate)。函數

        private static MemoryStream SerializeToMemoryStream(object objectGraph)
        {
            //一個流用來存放序列化對象
            var stream = new MemoryStream();
            //一個序列化格式化器
            var formater = new BinaryFormatter();
            //將對象序列化到Stream中
            formater.Serialize(stream, objectGraph);
            return stream;
        }

        private static object DeserializeFromMemory(Stream stream)
        {
            var formater = new BinaryFormatter();
            return formater.Deserialize(stream);
        }

SerializeToMemoryStream爲序列化方法,此處經過BinaryFormatter類將對象序列化到MemoryStream中,而後返回Stream對象。性能

DeserizlizeFromMemory爲反序列化方法,經過傳入的Stream,而後使用BinaryFormatter的Deserialize方法反序列化對象。測試

除了可使用BinaryFormatter進行字節流的序列化,還可使用XmlSerializer(將對象序列爲XML)和DataContratSerializer。字體

Serialize的第二個參數是一個對象的引用,理論上應該能夠是任何類型,無論.net的基本類型仍是其餘類型或者是咱們的自定義類型。若是是對象和對象的引用關係,Serizlize也是能夠一直序列化的,並且Serialize會很智能的序列化每一個對象都只序列化一次,防止進入無限循環。this

P.S. 1.Serialze方法其實能夠將對象序列化爲Stream,也就意味着不只能夠序列化爲MemoryStream,還能夠序列化爲FIleStream或者是其餘繼承自Stream的類型。

      2.除了上述的將一個對象序列化到一個Stream,也能夠將多個對象序列化中,仍是調用Serialize方法,第二個參數爲不一樣的對象便可;在反序列化的時候一樣的方法,只不過      強轉的類型指定爲須要的便可。

 序列化多個對象到Stream:

            MyClass class1 = new MyClass();
            MyClass2 class2=new MyClass2();
            formater.Serialize(stream,class1);
            formater.Serialize(stream,class2);

從Stream中反序列化多個對象:

            MyClass class1 =(MyClass) formater.Deserialize(stream);
            MyClass1 class2 = (MyClass1)formater.Deserialize(stream);

 2、控制序列化和反序列化

若是給類添加了SerializeAttribute,那麼類的全部實例字段(private、protected、public等)都會被序列化。可是,有時候類型中定義了一些不該序列化的實例字段。

通常狀況下,如下兩種狀況不但願序列化字段:

  • 字段含有反序列化後變得無效的信息。例如,假定一個對象包含到一個Windows內核對象(如文件、進程、線程、事件等),那麼在反序列化到另外一個進程或另外一臺機器以後,就會失去意義。
  • 字段含有很容易計算的信息。在這種狀況下,要選出那些無需序列化的字段,減小須要傳輸的數據,從而加強應用程序的性能。

使用NonSerializedAttribute特性來指明哪些字段無需序列化。

     [NonSerialized]
        private string _name;

p.s.[NoSerialized] 僅僅能添加在字段,或者是沒有get和set訪問器屬性上,對於有get和set這樣的屬性使用是不行的。不要緊使用[ScriptIgnore]特性標識屬性則能夠忽略JSON這樣的序列化、使用[XmlIgnoreAttribute]特性標識屬性則能夠忽略XmlSerializer的序列化操做。

雖然使用NonSerizlized特性可使字段不被序列化,可是在序列化或者反序列化的時候每每都會把值清空,或者是沒有一些但願的默認值,還好咱們可使用其餘的特性來輔助完成。

修改下上文中的MyClass:

[Serializable]
    class MyClass
    {
        [NonSerialized]
        public string _name;

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            _name = "Mario";
        }

        [OnDeserializing]
        private void OnDeserializing(StreamingContext context)
        {
            _name = "super";
        }

        [OnSerializing]
        private void OnSerializing(StreamingContext context)
        {
            _name = "listen";
        }

        [OnSerialized]
        private void OnSerialized(StreamingContext context)
        {
            _name = "fly";
        }

        public void Print()
        {
            Console.WriteLine(_name);
        }
    }

 在類中一共使用了四個特性,OnDeserialized、OnDeserializing、OnSerializing、OnSerialized,分別是反序列化後、反序列化前、序列化前、序列化後。不過,若是同時指定了OnDeserialized和OnDeserializing,那麼結果應該是OnDeserialized中的邏輯;同理,若是同時指定了OnSerializing和OnSerialized,那麼結果應該是OnSerialized中的邏輯。另外,在一個類中,僅僅能指定一個方法爲上述中的一個特性(即OnSerialized特性只能被一個方法使用、OnSerialized特性只能被一個方法使用,其他兩個同理),不然序列化或者反序列化則會出現異常。

P.S. 這些方法一般爲private的,而且參數爲StreamingContext。

       MyClass class1 = new MyClass();
            var stream = SerializeToMemoryStream(class1);
            class1.Print();
            stream.Position = 0;
            class1 = (MyClass)DesrializeFromMemory(stream);
            class1.Print();
            Console.Read();

 運行上述調用能夠發現,雖然咱們沒有將name屬性序列化,可是在序列化/反序列化以後仍是能夠輸出值的,若是你同時指定了OnDeserializing和OnDeserialized或者同時指定了OnSerializing和OnSerialized,那麼你會發現使用的都是後者的值,這也驗證了上述中的解釋。

有時候咱們的類可能會增長字段,但是呢,咱們已經序列化好的數據是舊的版本,因此在反序列化的時候就會出現異常,還好咱們也有辦法,給新加的字段都增長一個OptinalFieldAttribute特性,這樣當格式化器看到該attribute應用於一個字段時,就不會由於流中的數據不包含這個字段而出現異常。

3、序列化和反序列化的原理

爲了簡化格式化器的操做,在System.Runteime.Serialization中有一個FormatterServices類型。該類型只包含靜態方法,而且該類爲靜態類。

Serialize步驟:

  • 格式化器調用FormatterServices的GetSerializableMembers方法:
    public static MemberInfo[] GetSerializableMembers(Type type,StreamContext context);

    這個方法利用反射獲取類型的public和private實例字段(除了標識爲NonSerializedAttribute的字段除外)。方法返回由MemberInfo對象構成的一個數組,其中每一個元素都對應於一個可序列化的實例字段。

  • 對象被序列化,MemberInfo對象數組傳給FormatterServices的靜態方法GetObjectData:
    public static object[] GetObjectData(Object obj,MemberInfo[] members);

    這個方法返回一個Object數組,其中每一個元素都標識了被序列化的那個對象的一個字段的值。這個Object數組和MemberInfo數組是並行的;也就是說,Object數組中的元素0是MemberInfo數組中的元素0所標識的那個成員的值。

  • 格式化器將程序集標識和類型的完整名稱寫入流中。
  • 格式化器而後遍歷兩個數組中的元素,將每一個成員的名稱和值寫入流中。

Deserialize步驟:

  • 格式化器從流中讀取程序集標識和完整類型名稱。若是程序集當前沒有加載到AppDomain中,就加載它。若是程序集不能加載,則出現異常。若是程序集已經加載,格式化器將程序集標識信息和類型全名傳給FormatterServices的靜態方法GetTypeFromAssembly:
    public static Type GetTypeFromAssembly(Assembly assembly, string name);

    這個方法返回一個Type對象,表明要反序列化的那個對象的類型。

  • 格式化器調用FormatterServices的靜態方法GetUninitializedObject:
    public static Object GetUninitializedObject(Type type);

    這個方法爲一個新對象分配內存,並不爲對象調用構造函數。因此,對象的全部字段都被初始化爲null或者0;

  • 格式化器如今構造並初始化一個MemberInfo數組,一樣是調用FormatterServices的GetSerializableMembers方法。這個方法返回序列化好,須要反序列化的一組字段。
  • 格式化器根據流中包含的數據建立並初始化一個Object數組。
  • 將對新分配的對象、MemberInfo數組以及並行Object數組的傳給FomatterServices的靜態方法PopulateObjectMembers:
    public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object [] data);

    這個方法遍歷數組,將每一個字段初始化成對應的值。到這裏,就算反序列化結束了。

4、控制序列化/反序列化的數據

本文上述,有提到如何使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized以及NonSerialized和OptionalField特性進行控制序列化和反序列化。可是,格式化器內部使用反射,而反射的速度是比較慢的,因此增長了序列化和反序列化對象所花的時間。爲了對序列化和反序列化徹底的控制,而且不使用反射,那麼咱們的類型能夠實現ISerializable接口,此接口僅僅有一個方法:

public Interface ISerializable
{
  void GetObjectData(SerializationInfo info, StreamContext context);
}

一旦類型實現了此接口,全部派生類型也必須實現它,並且派生類型必須保證調用基類的GetOBjectData方法和特殊的構造器。除此以外,一旦類型實現了該接口,則永遠不能刪除它,不然會失去與派生類的兼容性。

ISerializable接口和特殊構造器旨在由格式化器使用。可是,任何代碼均可能調用GetObjectData,則可能返回敏感數據。另外,其餘代碼可能構造一個對象,並傳入損壞的數據。所以,建議將以下的attribute應用於GetObjectData方法和特殊構造器:

[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]

 格式化器序列化一個對象時,會檢查每一個對象。若是發現一個對象的類型實現了ISerializable接口,格式化器就會忽略全部定製attribute,改成構造一個新的SerializationInfo對象,這個對象包含了要實際爲對象序列化的值的集合。

構造一個SerializationInfo時,格式化器要兩個參數:Type和IFormatterConverter。Type參數標識要序列化的對象。爲了惟一性地標識一個類型,須要兩個部分的信息:類型的字符串名稱及其程序集的標識。一個SerializationInfo對象構造好以後,會包含類型的全名(即Type的FullName),並將這個字符串存儲到一個私有字段中。爲了獲取類型的全名,可以使用SerializationInfo的FullTypeName屬性。經過調用SerializationInfo的SetType方法,傳遞目標Type對象的引用,用於設置FullTypeName和AssemblyName屬性。

構造好並初始化SerializationInfo對象後,格式化器調用類型的GetObjectData方法,傳遞SeriializationInfo對象。GetObjectData方法負責決定須要序列化的信息,而後將這些信息添加到SerializationInfo中。GetObjectData調用SerializationInfo類型的AddValue方法來指定要序列化的信息。須要對每一個要添加的數據,都進行AddValue方法的調用。 

下面代碼展現了Dictionary<TKey,TValue>類型如何實現ISerializable和IDeserializationCallback接口來控制其對象的序列化和反序列化工做。

4、在基類沒有實現ISerializable的狀況下定義一個實現它的類型

以前提到,若是基類實現了ISerializable接口,那麼它的派生類也必須實現ISerializable接口,同時還要調用基類的GetObjectData方法和特殊構造器。(見上文紅色字體)
可是,你可能要定義一個類型來控制它的序列化,但它的基類沒有實現ISerializable接口。在這種狀況下,派生類必須手動序列化基類的字段,具體的作法是獲取它們的值,並把這些值添加到SerializationInfo集合中。而後,在特殊構造器中,還必須從集合中取出值,並以某種方式設置基類的字段。若是基類的字段是public或者protected字段,還容易實現。但,若是基類的private字段,那麼則很難實現。

如下代碼實現如何正確實現ISerializable的GetObjectData方法和特殊的構造器:

    [Serializable]
        class Base
        {
            protected string name = "Mario";
            public Base()
            {
            }
        }

        [Serializable]
        class Derived : Base, ISerializable
        {
            private DateTime _date = DateTime.Now;
            public Derived() { }

      //若是這個構造器不存在,則會引起一個SerializationException異常
      //若是此類不是密封類,這個構造器就應該是protected的 [SecurityPermission(SecurityAction.Demand, SerializationFormatter
= true)] private Derived(SerializationInfo info, StreamingContext context) { Type baseType = this.GetType().BaseType; MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context); for (int i = 0; i < memberInfos.Length; i++) { FieldInfo fieldInfo = (FieldInfo)memberInfos[i]; fieldInfo.SetValue(this, info.GetValue(baseType.FullName + "+" + fieldInfo.Name, fieldInfo.FieldType)); } _date = info.GetDateTime("Date"); } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Data", _date); Type baseType = this.GetType().BaseType; MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType,context); for (int i = 0; i < memberInfos.Length; i++) { info.AddValue(baseType.FullName + "+" + memberInfos[i].Name, ((FieldInfo)memberInfos[i]).GetValue(this)); } } public override string ToString() { return string.Format("Name={0},Date={}", name, _date); } }

在代碼中,有一個名爲Base的基類,它只用Serializable特性標識。其派生類Derived類,也使用了Serializable特性,同時還實現了ISerializable接口。同時兩個類還定義了本身的字段,調用SerializationInfo的AddValue方法進行序列化和反序列化。

 

解釋:

序列化: 每一個AddValue方法都獲取一個String名稱和一些數據。數據通常是簡單的類型,固然咱們也能夠傳遞object引用。GetObjectData添加好全部必要的序列化信息以後,會返回至格式化器。如今,格式化器獲取已經添加到SerializationInfo對象的全部值,並把它們都序列化到流中。同時,咱們還向GetObjectData方法中傳遞了另一個參數StreamingContext對象的實例。固然,大多數類型的GetObjectData方法都忽略了此參數,下文詳細說明。

反序列化:格式化器從流中提取一個對象時,會爲新對象分配內存(經過FormatterService.GetUninitializedObject方法)。最初,此對象的全部字段都爲0或者是null。而後,格式化器檢查類型是否實現了ISerializable接口。若是存在此接口,格式化器則會嘗試調用咱們定義的特殊構造函數,它的參數和GetObjectData是一致的。

若是類是密封類,則建議將此特殊構造聲明爲private,這樣就能夠防止其餘代碼調用它。若是不是密封類,則應該將這個特殊構造器聲明爲protected,保證派生類能夠調用它。切記,不管這個特殊構造器是如何聲明的,格式化器均可以調用它的。

構造器獲取對一個SerializationInfo對象的引用,在這個SerializationInfo對象中,包含了對象(要序列化的對象)序列化時添加的全部值。特殊構造器可調用GetBoolean,GetChar,GetByte,GetInt32和GetValue等任何一個方法,向他傳遞與序列化一個值所用的名稱對應的一個字符串。以上的每一個方法返回的值再用於初始化新對象的各個字段。

反序列化一個對象的字段時,應調用和對象序列化時傳給AddValue方法的值得類型匹配的一個Get方法。也就是說,若是GetObjectData方法調用AddValue時傳遞的是一個Int32值,那麼在反序列化對象的時候,也應該爲同一個值調用GetInt32方法。若是值在流中的類型和你要獲取的類型不匹配,格式化器則會嘗試用IFormatterConverter對象將流中的值轉換爲你指定的類型。

上文中提到,構造SerializationInfo對象時,須要傳遞Type和IFormatterConverter接口的對象(此時,它是重點,不要被Type勾引走)。因爲格式化器負責構造SerializationInfo對象,因此要由它選擇它須要的IFormatterConverter。.Net的BinaryFormatter和SoapFormatter構造的就是一個FormatterConverter類型,.Net的格式化器沒有提供一個讓你能夠選擇的IFormatterConverter的實現。

FormatterConverter類型調用System.Convert類的各類靜態方法在不一樣的類型之間進行轉換,好比講一個Int16轉換爲Int32。然而,爲了在其餘任意類型之間轉換一個值,FormatterConverter須要調用Convert的ChangeType方法將序列化好的類型轉換爲一個IConvertible接口,而後再調用恰當的接口的方法。因此,要容許一個可序列化類型的對象反序列化成一個不一樣的類型,能夠考慮讓本身的類型實現IConvertible接口。切記,只有在反序列化對象時調用Get方法,而且發現了類型和流中的值得類型不匹配時候,纔會使用FormatterConverter對象。

特殊構造器也能夠不調用上面的各類Get方法,而是調用GetEnumerator。此方法會返回一個SerializationInfoEnumerator對象,可以使用該對象遍歷SerializationInfo對象中包含的全部的值。枚舉的每一個值都是一個SerializationEntry對象。

固然,咱們徹底能夠自定義一個類型,讓它實現ISerializable的GetObjectData方法和特殊構造器一個類型派生。若是咱們的類型實現了ISerializable,那麼能夠在咱們實現的GetObjectData方法和特殊構造器中,必須調用基類中的同名方法,以確保對象正確序列化和反序列化。這一點是必須的哦,不然對象時不能正確序列化和反序列化。

若是咱們的派生類型中沒有其餘的額外字段,固然也沒有特殊的序列化和反序列化需求,就不用事先ISerializable接口。和其餘接口成員類似,GetObjectData是virtual的,調用它能夠正確的序列化對象。格式化器將特殊構造器視爲「已虛擬化」,也就是說,反序列化過程當中,格式化器會檢查要實例的類型,若是那個類型沒有提供特殊的特殊構造器,則會看其基類是否存在,知道找到一個實現了特殊構造器的一個類。

 

注意:特殊構造器中的代碼通常會從傳給 它的SerializationInfo對象中提取字段。提取了字段後,不能保證對象已徹底反序列化,因此特殊構造器中的代碼不該嘗試操縱它提取的對象。若是咱們的類型必須訪問提取的一個對象中的成員,最好咱們的類型提供一個應用了OnDeserialized特性的方法,或者讓咱們的類型實現IDeserializationCallback接口的OnDeserialization方法。調用該方法時,全部對象的字段都已經設置好。然而,對於多個對象來講,它們的OnDeserialized或OnDeserialization方法的調用順序是沒有保障的。因此,雖然字段可能已經初始化,但咱們仍然不知道被引用的對象是否已徹底反序列化好(若是那個被引用的對象也提供了一個OnDeserialized方法或者實現了IDeserializationCallback)。

P.S. 必須調用AddValue方法的某個重載版本爲本身的類型添加序列化信息。若是一個字段的類型實現了ISerializable接口,就不要在字段上調用GetObjectData,而應該調用AddValue來添加字段。格式化器會發現字段的類型實現了ISerializable,會自動調用GetObjectData。若是本身在字段上調用了GetObjectData,格式化器則不會知道在對流進行反序列化時建立一個新對象。

5、將類型序列化爲不一樣的類型以及將對象反序列化爲不一樣的對象

      [Serializable]
        public class Student : ISerializable
        {
            private string _name;

            public string Name
            {
                get { return _name; }
                set { _name = value; }
            }
            [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
            public void GetObjectData(SerializationInfo info, StreamingContext context)
            {
                info.SetType(typeof(SerializationHelper));
            }
        }

        [Serializable]
        public class SerializationHelper : IObjectReference
        {
            public object GetRealObject(StreamingContext context)
            {
                return "新的類型哦";
            }
        }

上述代碼中一個咱們的數據類Student,還有一個序列化幫助類,其中Student類就是咱們要序列化的類,幫助類就是爲了告訴代碼咱們要把Student類序列化爲它,而且再反序列化的時候也應該是它。
測試下:

   static void Main(string[] args)
        {
            Student student = new Student { Name = "馬里奧" };
            using (var stream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, student);
                stream.Position = 0;

                var deserializeValue = formatter.Deserialize(stream);
                Console.Write(deserializeValue.ToString());
                Console.Read();
            }
        }

能夠看到結果:

P.S. ISerializable:容許對象控制其本身的序列化和反序列化過程。

   IObjectReference:指示當前接口實施者是對另外一個對象的引用。

好了,序列化和反序列化的東西說的也差很少了,你們有什麼更好的想法能夠和我交流。

相關文章
相關標籤/搜索