C# Serializable
2.
Serializable在C#中的做用.NET 中的對象序列化 簡介 序列化是指將對象實例的狀態存儲到存儲媒體的過程。在此過程當中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換爲字節流,而後再把字節流寫入數據流。在隨後對對象進行反序列化時,將建立出與原對象徹底相同的副本。 在面向對象的環境中實現序列化機制時,必須在易用性和靈活性之間進行一些權衡。只要您對此過程有足夠的控制能力,就可使該過程在很大程度上自動進行。例如,簡單的二進制序列化不能知足須要,或者,因爲特定緣由須要肯定類中那些字段須要序列化。如下各部分將探討 .NET 框架提供的可靠的序列化機制,並着重介紹使您能夠根據須要自定義序列化過程的一些重要功能。 持久存儲 咱們常常須要將對象的字段值保存到磁盤中,並在之後檢索此數據。儘管不使用序列化也能完成這項工做,但這種方法一般很繁瑣並且容易出錯,而且在須要跟蹤對象的層次結構時,會變得愈來愈複雜。能夠想象一下編寫包含大量對象的大型業務應用程序的情形,程序員不得不爲每個對象編寫代碼,以便將字段和屬性保存至磁盤以及從磁盤還原這些字段和屬性。序列化提供了輕鬆實現這個目標的快捷方法。 公共語言運行時 (CLR) 管理對象在內存中的分佈,.NET 框架則經過使用反射提供自動的序列化機制。對象序列化後,類的名稱、程序集以及類實例的全部數據成員均被寫入存儲媒體中。對象一般用成員變量來存儲對其餘實例的引用。類序列化後,序列化引擎將跟蹤全部已序列化的引用對象,以確保同一對象不被序列化屢次。.NET 框架所提供的序列化體系結構能夠自動正確處理對象圖表和循環引用。對對象圖表的惟一要求是,由正在進行序列化的對象所引用的全部對象都必須標記爲 Serializable(請參閱基本序列化)。不然,當序列化程序試圖序列化未標記的對象時將會出現異常。 當反序列化已序列化的類時,將從新建立該類,並自動還原全部數據成員的值。 按值封送 對象僅在建立對象的應用程序域中有效。除非對象是從 MarshalByRefObject 派生獲得或標記爲 Serializable,不然,任何將對象做爲參數傳遞或將其做爲結果返回的嘗試都將失敗。若是對象標記爲 Serializable,則該對象將被自動序列化,並從一個應用程序域傳輸至另外一個應用程序域,而後進行反序列化,從而在第二個應用程序域中產生出該對象的一個精確副本。此過程一般稱爲按值封送。 若是對象是從 MarshalByRefObject 派生獲得,則從一個應用程序域傳遞至另外一個應用程序域的是對象引用,而不是對象自己。也能夠將從 MarshalByRefObject 派生獲得的對象標記爲 Serializable。遠程使用此對象時,負責進行序列化並已預先配置爲 SurrogateSelector 的格式化程序將控制序列化過程,並用一個代理替換全部從 MarshalByRefObject 派生獲得的對象。若是沒有預先配置爲 SurrogateSelector,序列化體系結構將聽從下面的標準序列化規則(請參閱序列化過程的步驟)。 基本序列化 要使一個類可序列化,最簡單的方法是使用 Serializable 屬性對它進行標記,以下所示: [Serializable]
public
class
MyObject
...
{ publicint n1 =0; publicint n2 =0; public String str =null; }
如下代碼片斷說明了如何將此類的一個實例序列化爲一個文件: 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 序列化程序。 將對象還原到它之前的狀態也很是容易。首先,建立格式化程序和流以進行讀取,而後讓格式化程序對對象進行反序列化。如下代碼片斷說明了如何進行此操做。 IFormatter formatter
=
new
BinaryFormatter(); Stream stream
=
new
FileStream(
"
MyFile.bin
"
, FileMode.Open, FileAccess.Read, FileShare.Read); MyObject obj
=
(MyObject) formatter.Deserialize(fromStream); stream.Close();
//
下面是證實
Console.WriteLine(
"
n1: {0}
"
, obj.n1); Console.WriteLine(
"
n2: {0}
"
, obj.n2); Console.WriteLine(
"
str: {0}
"
, obj.str); 上面所使用的 BinaryFormatter 效率很高,能生成很是緊湊的字節流。全部使用此格式化程序序列化的對象也可以使用它進行反序列化,對於序列化將在 .NET 平臺上進行反序列化的對象,此格式化程序無疑是一個理想工具。須要注意的是,對對象進行反序列化時並不調用構造函數。對反序列化添加這項約束,是出於性能方面的考慮。可是,這違反了對象編寫者一般採用的一些運行時約定,所以,開發人員在將對象標記爲可序列化時,應確保考慮了這一特殊約定。 若是要求具備可移植性,請使用 SoapFormatter。所要作的更改只是將以上代碼中的格式化程序換成 SoapFormatter,而 Serialize 和 Deserialize 調用不變。對於上面使用的示例,該格式化程序將生成如下結果。
<
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
...
{ publicint n3; }
使用序列化屬性很是方便,可是它存在上述的一些限制。有關什麼時候標記類以進行序列化(由於類編譯後就沒法再序列化),請參考有關說明(請參閱下面的序列化規則)。 選擇性序列化 類一般包含不該被序列化的字段。例如,假設某個類用一個成員變量來存儲線程 ID。當此類被反序列化時,序列化此類時所存儲的 ID 對應的線程可能再也不運行,因此對這個值進行序列化沒有意義。能夠經過使用 NonSerialized 屬性標記成員變量來防止它們被序列化,以下所示: [Serializable]
public
class
MyObject
...
{ publicint n1; [NonSerialized] publicint n2; public String str; }
自定義序列化 能夠經過在對象上實現 ISerializable 接口來自定義序列化過程。這一功能在反序列化後成員變量的值失效時尤爲有用,可是須要爲變量提供值以重建對象的完整狀態。要實現 ISerializable,須要實現 GetObjectData 方法以及一個特殊的構造函數,在反序列化對象時要用到此構造函數。如下代碼示例說明了如何在前一部分中提到的 MyObject 類上實現 ISerializable。 [Serializable]
public
class
MyObject : ISerializable
...
{ publicint n1; publicint n2; public String str; public MyObject() ...{ } protected MyObject(SerializationInfo info, StreamingContext context) ...{ n1 = info.GetInt32("i"); n2 = info.GetInt32("j"); str = info.GetString("k"); } publicvirtualvoid 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,則應調用基類的構造函數,以使基礎對象能夠還原其變量。 若是從實現了 ISerializable 的類派生出一個新的類,則只要新的類中含有任何須要序列化的變量,就必須同時實現構造函數以及 GetObjectData 方法。如下代碼片斷顯示瞭如何使用上文所示的 MyObject 類來完成此操做。 [Serializable]
public
class
ObjectTwo : MyObject
...
{ publicint num; public ObjectTwo() : base() ...{ } protected ObjectTwo(SerializationInfo si, StreamingContext context) : base(si,context) ...{ num = si.GetInt32("num"); } publicoverridevoid GetObjectData(SerializationInfo si, StreamingContext context) ...{ base.GetObjectData(si,context); si.AddValue("num", num); } }
切記要在反序列化構造函數中調用基類,不然,將永遠不會調用基類上的構造函數,而且在反序列化後也沒法構建完整的對象。 對象被完全從新構建,可是在反系列化過程當中調用方法可能會帶來不良的反作用,由於被調用的方法可能引用了在調用時還沒有反序列化的對象引用。若是正在進行反序列化的類實現了 IDeserializationCallback,則反序列化整個對象圖表後,將自動調用 OnSerialization 方法。此時,引用的全部子對象均已徹底還原。有些類不使用上述事件偵聽器,很難對它們進行反序列化,散列表即是一個典型的例子。在反序列化過程當中檢索關鍵字
/
值對很是容易,可是,因爲沒法保證從散列表派生出的類已反序列化,因此把這些對象添加回散列表時會出現一些問題。所以,建議目前不要在散列表上調用方法。 序列化過程的步驟 在格式化程序上調用 Serialize 方法時,對象序列化按照如下規則進行: 檢查格式化程序是否有代理選取器。若是有,檢查代理選取器是否處理指定類型的對象。若是選取器處理此對象類型,將在代理選取器上調用 ISerializable.GetObjectData。 若是沒有代理選取器或有卻不處理此類型,將檢查是否使用 Serializable 屬性對對象進行標記。若是未標記,將會引起 SerializationException。 若是對象已被正確標記,將檢查對象是否實現了 ISerializable。若是已實現,將在對象上調用 GetObjectData。 若是對象未實現 Serializable,將使用默認的序列化策略,對全部未標記爲 NonSerialized 的字段都進行序列化。 版本控制 .NET 框架支持版本控制和並排執行,而且,若是類的接口保持一致,全部類都可跨版本工做。因爲序列化涉及的是成員變量而非接口,因此,在向要跨版本序列化的類中添加成員變量,或從中刪除變量時,應謹慎行事。特別是對於未實現 ISerializable 的類更應如此。若當前版本的狀態發生了任何變化(例如添加成員變量、更改變量類型或更改變量名稱),都意味着若是同一類型的現有對象是使用早期版本進行序列化的,則沒法成功對它們進行反序列化。 若是對象的狀態須要在不一樣版本間發生改變,類的做者能夠有兩種選擇: 實現 ISerializable。這使您能夠精確地控制序列化和反序列化過程,在反序列化過程當中正確地添加和解釋將來狀態。 使用 NonSerialized 屬性標記不重要的成員變量。僅當預計類在不一樣版本間的變化較小時,纔可以使用這個選項。例如,把一個新變量添加至類的較高版本後,能夠將該變量標記爲 NonSerialized,以確保該類與早期版本保持兼容。 序列化規則 因爲類編譯後便沒法序列化,因此在設計新類時應考慮序列化。須要考慮的問題有:是否必須跨應用程序域來發送此類?是否要遠程使用此類?用戶將如何使用此類?也許他們會從個人類中派生出一個須要序列化的新類。只要有這種可能性,就應將類標記爲可序列化。除下列狀況之外,最好將全部類都標記爲可序列化: 全部的類都永遠也不會跨越應用程序域。若是某個類不要求序列化但須要跨越應用程序域,請從 MarshalByRefObject 派生此類。 類存儲僅適用於其當前實例的特殊指針。例如,若是某個類包含非受控的內存或文件句柄,請確保將這些字段標記爲 NonSerialized 或根本不序列化此類。 某些數據成員包含敏感信息。在這種狀況下,建議實現 ISerializable 並僅序列化所要求的字段。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1735611