C# 特性(Attribute)之Serializable特性


本文參考自Serializable 作用,純屬讀書筆記,加深記憶

介紹之前,先說一個重要的知識點:

Serializable屬性並不序列化類,它只是一個標簽。至於如何序列化,各種序列化類各自有各自的做法,它們只是讀取這個標簽而已,之后就按照自己的方式去序列化,例如某個應用程序會反射目標對象的類型的所有Field和Property,看看它是否實現了ISerializable,如果實現了就調用它。你可以看看關於實現ISerializable接口來使自己的類可以被序列化的做法。某些應用程序查找SerializableAttribute屬性來避免去花時間反射對象的屬性,當它看到一個對象沒有標記為可序列化,就會直接報錯,而不會去花時間反射字段。
但是像WCF,即使你不去標記,它也會花時間去反射字段。它已經不在乎SerializableAttribute了。
SerializableAttribute僅是一個標記而已,它並不執行序列化動作。

 

1、Serializable特性的作用

序列化的attribute,是為了利用序列化的技術 准備用於序列化的對象必須設置 [System.Serializable] 標簽,該標簽指示一個類可以序列化。 便於在網絡中傳輸和保存這個標簽是類可以被序列化的特性,表示這個類可以被序列化。

什么叫序列化?
我們都知道對象是暫時保存在內存中的,不能用U盤考走了,有時為了使用介質轉移對象,並且把對象的狀態保持下來,就需要把對象保存下來,這個過程就叫做序列化,通俗點,就是把人的魂(對象)收伏成一個石子(可傳輸的介質)
什么叫反序列化?
就是再把介質中的東西還原成對象,把石子還原成人的過程。
在進行這些操作的時候都需要這個可以被序列化,要能被序列化,就得給類頭加[Serializable]特性。
通常網絡程序為了傳輸安全才這么做。

 

2、關於序列化和反序列化的執行過程和原理

持久存儲

我們經常需要將對象的字段值保存到磁盤中,並在以后檢索此數據。盡管不使用序列化也能完成這項工作,但這種方法通常很繁瑣而且容易出錯,並且在需要跟蹤對象的層次結構時,會變得越來越復雜。可以想象一下編寫包含大量對象的大型業務應用程序的情形,程序員不得不為每一個對象編寫代碼,以便將字段和屬性保存至磁盤以及從磁盤還原這些字段和屬性。序列化提供了輕松實現這個目標的快捷方法。

公共語言運行時 (CLR) 管理對象在內存中的分布,.NET 框架則通過使用反射提供自動的序列化機制。對象序列化后,類的名稱、程序集以及類實例的所有數據成員均被寫入存儲媒體中。對象通常用成員變量來存儲對其他實例的引用。類序列化后,序列化引擎將跟蹤所有已序列化的引用對象,以確保同一對象不被序列化多次。.NET 框架所提供的序列化體系結構可以自動正確處理對象圖表和循環引用。對對象圖表的唯一要求是,由正在進行序列化的對象所引用的所有對象都必須標記為 Serializable(請參閱基本序列化)。否則,當序列化程序試圖序列化未標記的對象時將會出現異常。

當反序列化已序列化的類時,將重新創建該類,並自動還原所有數據成員的值。

按值封送 對象僅在創建對象的應用程序域中有效。除非對象是從 MarshalByRefObject 派生得到或標記為 Serializable,否則,任何將對象作為參數傳遞或將其作為結果返回的嘗試都將失敗。如果對象標記為 Serializable,則該對象將被自動序列化,並從一個應用程序域傳輸至另一個應用程序域,然后進行反序列化,從而在第二個應用程序域中產生出該對象的一個精確副本。此過程通常稱為按值封送。

如果對象是從 MarshalByRefObject 派生得到,則從一個應用程序域傳遞至另一個應用程序域的是對象引用,而不是對象本身。也可以將從 MarshalByRefObject 派生得到的對象標記為 Serializable。遠程使用此對象時,負責進行序列化並已預先配置為 SurrogateSelector 的格式化程序將控制序列化過程,並用一個代理替換所有從 MarshalByRefObject 派生得到的對象。如果沒有預先配置為 SurrogateSelector,序列化體系結構將遵從下面的標准序列化規則(請參閱序列化過程的步驟)。

 

3、基本序列化例子

(1)、首先需要一個可序列化的類,代碼如下:

    [Serializable]
    public class MyObject
    {
        public int n1 { get; set; }
        public int n2 { get; set; }
        public string str { get; set; }
    }

通過添加[Serializable]特性確保當前類可以被實例化。

(2)、將類實例序列化進流,代碼如下:

        public static void SerializableObj()
        {
            MyObject obj = new MyObject();
            obj.n1 = 1;
            obj.n2 = 24;
            obj.str = "一些字符串";
            IFormatter formatter = new BinaryFormatter();
            Stream stream = new FileStream("MyFile.bin", FileMode.Create,
            FileAccess.Write, FileShare.None);
            formatter.Serialize(stream, obj);
            stream.Close();
        }

本例使用二進制格式化程序進行序列化。您只需創建一個要使用的流和格式化程序的實例,然后調用格式化程序的 Serialize 方法。流和要序列化的對象實例作為參數提供給此調用。類中的所有成員變量(甚至標記為 private 的變量)都將被序列化,但這一點在本例中未明確體現出來。在這一點上,二進制序列化不同於只序列化公共字段的 XML 序列化程序

(3)、將對應的類實例進行反序列化

        public static MyObject DeSerializableObj()
        {
            IFormatter formatter = new BinaryFormatter();
            Stream stream = new FileStream("MyFile.bin", FileMode.Open,
            FileAccess.Read, FileShare.Read);
            MyObject obj = (MyObject)formatter.Deserialize(stream);
            stream.Close();
            return obj;
        }

(4)、測試過程是否成功

        static void Main(string[] args){
            SerializableObj();
            MyObject obj = DeSerializableObj();
            Console.WriteLine("n1: {0}", obj.n1);
            Console.WriteLine("n2: {0}", obj.n2);
            Console.WriteLine("str: {0}", obj.str);
        }

輸出:

說明整個序列化和反序列化的過程成功!!!注意:需要序列化的類必須將[Serializable]特性,否則會報錯!!!

上面所使用的 BinaryFormatter 效率很高,能生成非常緊湊的字節流。所有使用此格式化程序序列化的對象也可使用它進行反序列化,對於序列化將在 .NET 平台上進行反序列化的對象,此格式化程序無疑是一個理想工具。需要注意的是,對對象進行反序列化時並不調用構造函數。對反序列化添加這項約束,是出於性能方面的考慮。但是,這違反了對象編寫者通常采用的一些運行時約定,因此,開發人員在將對象標記為可序列化時,應確保考慮了這一特殊約定。

 

4、序列化對象的可移植性

如果要求序列化完之后的對象,具有可移植性,就是你希望在.Net平台下序列化的實體類對象,在Java平台下也能適用。

那就用SoapFormatter,而 Serialize 和 Deserialize 調用不變,代碼如下:

        public static void SerializableObj()
        {
            MyObject obj = new MyObject();
            obj.n1 = 1;
            obj.n2 = 24;
            obj.str = "一些字符串";
            SoapFormatter formatter = new SoapFormatter();
            Stream stream = new FileStream("MyFile.bin", FileMode.Create,
            FileAccess.Write, FileShare.None);
            stream.Close();
        }

序列化結果如下:

<SOAP-ENV:Envelope
   xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
   xmlns:xsd="http://www.w3.org/2001/XMLSchema"
   xmlns:SOAP- ENC=http://schemas.xmlsoap.org/soap/encoding/
   xmlns:SOAP- ENV=http://schemas.xmlsoap.org/soap/envelope/
   SOAP-ENV:encodingStyle=
   "http://schemas.microsoft.com/soap/encoding/clr/1.0
  http://schemas.xmlsoap.org/soap/encoding/"
   xmlns:a1="http://schemas.microsoft.com/clr/assem/ToFile">

   <SOAP-ENV:Body>
     <a1:MyObject id="ref-1">
       <n1>1</n1>
       <n2>24</n2>
       <str id="ref-3">一些字符串</str>
     </a1:MyObject>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

需要注意的是,無法繼承 Serializable 屬性。如果從 MyObject 派生出一個新的類,則這個新的類也必須使用該屬性進行標記,否則將無法序列化。例如,如果試圖序列化以下類實例,將會顯示一個 SerializationException,說明 MyStuff 類型未標記為可序列化。

public class MyStuff : MyObject
{
   public int n3;
}

使用序列化屬性非常方便,但是它存在上述的一些限制。有關何時標記類以進行序列化(因為類編譯后就無法再序列化),請參考有關說明(請參閱下面的序列化規則)。

 

5、選擇性序列化

類通常包含不應被序列化的字段。例如,假設某個類用一個成員變量來存儲線程 ID。當此類被反序列化時,序列化此類時所存儲的 ID 對應的線程可能不再運行,所以對這個值進行序列化沒有意義。可以通過使用 NonSerialized 屬性標記成員變量來防止它們被序列化,

[Serializable]
public class MyObject
{
   public int n1;
   [NonSerialized] public int n2;
   public String str;
}

 

6、自定義序列化

可以通過在對象上實現 ISerializable 接口來自定義序列化過程。這一功能在反序列化后成員變量的值失效時尤其有用,但是需要為變量提供值以重建對象的完整狀態。要實現 ISerializable,需要實現 GetObjectData 方法以及一個特殊的構造函數,在反序列化對象時要用到此構造函數。以下代碼示例說明了如何在前一部分中提到的 MyObject 類上實現 ISerializable。

    [Serializable]
    public class MyObject : ISerializable
    {
        public int n11;
        public int n22;
        public String str1;

        public MyObject()
        {
        }

        protected MyObject(SerializationInfo info, StreamingContext context)
        {
            n1 = info.GetInt32("i");
            n2 = info.GetInt32("j");
            str = info.GetString("k");
        }


        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("i", n1);
            info.AddValue("j", n2);
            info.AddValue("k", str);

        }
    }

在序列化過程中調用 GetObjectData 時,需要填充方法調用中提供的 SerializationInfo 對象。只需按名稱/值對的形式添加將要序列化的變量。其名稱可以是任何文本。只要已序列化的數據足以在反序列化過程中還原對象,便可以自由選擇添加至 SerializationInfo 的成員變量。如果基對象實現了 ISerializable,則派生類應調用其基對象的 GetObjectData 方法。

需要強調的是,將 ISerializable 添加至某個類時,需要同時實現 GetObjectData 以及特殊的構造函數。如果缺少 GetObjectData,編譯器將發出警告。但是,由於無法強制實現構造函數,所以,缺少構造函數時不會發出警告。如果在沒有構造函數的情況下嘗試反序列化某個類,將會出現異常。在消除潛在安全性和版本控制問題等方面,當前設計優於 SetObjectData 方法。例如,如果將 SetObjectData 方法定義為某個接口的一部分,則此方法必須是公共方法,這使得用戶不得不編寫代碼來防止多次調用 SetObjectData 方法。可以想象,如果某個對象正在執行某些操作,而某個惡意應用程序卻調用此對象的 SetObjectData 方法,將會引起一些潛在的麻煩。

在反序列化過程中,使用出於此目的而提供的構造函數將 SerializationInfo 傳遞給類。對象反序列化時,對構造函數的任何可見性約束都將被忽略,因此,可以將類標記為 public、protected、internal 或 private。一個不錯的辦法是,在類未封裝的情況下,將構造函數標記為 protect。如果類已封裝,則應標記為 private。要還原對象的狀態,只需使用序列化時采用的名稱,從 SerializationInfo 中檢索變量的值。如果基類實現了 ISerializable,則應調用基類的構造函數,以使基礎對象可以還原其變量。

 

7、從實現了 ISerializable 的類派生出一個新的類

果從實現了 ISerializable 的類派生出一個新的類,則只要新的類中含有任何需要序列化的變量,就必須同時實現構造函數以及 GetObjectData 方法。以下代碼片段顯示了如何使用上文所示的 MyObject 類來完成此操作。

[Serializable]
public class ObjectTwo : MyObject
{
   public int num;

   public ObjectTwo() : base()
   {
   }

   protected ObjectTwo(SerializationInfo si, StreamingContext context) :
base(si,context)
   {
     num = si.GetInt32("num");
   }

   public override void GetObjectData(SerializationInfo si,
StreamingContext context)
   {
     base.GetObjectData(si,context);
     si.AddValue("num", num);
   }
}

切記要在反序列化構造函數中調用基類,否則,將永遠不會調用基類上的構造函數,並且在反序列化后也無法構建完整的對象。

對象被徹底重新構建,但是在反系列化過程中調用方法可能會帶來不良的副作用,因為被調用的方法可能引用了在調用時尚未反序列化的對象引用。如果正在進行反序列化的類實現了 IDeserializationCallback,則反序列化整個對象圖表后,將自動調用 OnSerialization 方法。此時,引用的所有子對象均已完全還原。有些類不使用上述事件偵聽器,很難對它們進行反序列化,散列表便是一個典型的例子。在反序列化過程中檢索關鍵字/值對非常容易,但是,由於無法保證從散列表派生出的類已反序列化,所以把這些對象添加回散列表時會出現一些問題。因此,建議目前不要在散列表上調用方法。

 

8、序列化過程的步驟

在格式化程序上調用 Serialize 方法時,對象序列化按照以下規則進行:

檢查格式化程序是否有代理選取器。如果有,檢查代理選取器是否處理指定類型的對象。如果選取器處理此對象類型,將在代理選取器上調用 ISerializable.GetObjectData。
如果沒有代理選取器或有卻不處理此類型,將檢查是否使用 Serializable 屬性對對象進行標記。如果未標記,將會引發 SerializationException。
如果對象已被正確標記,將檢查對象是否實現了 ISerializable。如果已實現,將在對象上調用 GetObjectData。
如果對象未實現 Serializable,將使用默認的序列化策略,對所有未標記為 NonSerialized 的字段都進行序列化。
版本控制
.NET 框架支持版本控制和並排執行,並且,如果類的接口保持一致,所有類均可跨版本工作。由於序列化涉及的是成員變量而非接口,所以,在向要跨版本序列化的類中添加成員變量,或從中刪除變量時,應謹慎行事。特別是對於未實現 ISerializable 的類更應如此。若當前版本的狀態發生了任何變化(例如添加成員變量、更改變量類型或更改變量名稱),都意味着如果同一類型的現有對象是使用早期版本進行序列化的,則無法成功對它們進行反序列化。

如果對象的狀態需要在不同版本間發生改變,類的作者可以有兩種選擇:

實現 ISerializable。這使您可以精確地控制序列化和反序列化過程,在反序列化過程中正確地添加和解釋未來狀態。
使用 NonSerialized 屬性標記不重要的成員變量。僅當預計類在不同版本間的變化較小時,才可使用這個選項。例如,把一個新變量添加至類的較高版本后,可以將該變量標記為 NonSerialized,以確保該類與早期版本保持兼容。
序列化規則
由於類編譯后便無法序列化,所以在設計新類時應考慮序列化。需要考慮的問題有:是否必須跨應用程序域來發送此類?是否要遠程使用此類?用戶將如何使用此類?也許他們會從我的類中派生出一個需要序列化的新類。只要有這種可能性,就應將類標記為可序列化。除下列情況以外,最好將所有類都標記為可序列化:

所有的類都永遠也不會跨越應用程序域。如果某個類不要求序列化但需要跨越應用程序域,請從 MarshalByRefObject 派生此類。
類存儲僅適用於其當前實例的特殊指針。例如,如果某個類包含非受控的內存或文件句柄,請確保將這些字段標記為 NonSerialized 或根本不序列化此類。
某些數據成員包含敏感信息。在這種情況下,建議實現 ISerializable 並僅序列化所要求的字段。


免責聲明!

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



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