什么是序列化? ---.net的運行時環境用來支持用戶定義類型的流化的機制。它是將對象實例的狀態存儲到存儲媒體的過程。在此過程中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換為字節流,然后再把字節流寫入數據流。在隨后對對象進行反序列化時,將創建出與原對象完全相同的副本。
序列化的目的: 1、以某種存儲形式使自定義對象持久化; 2、將對象從一個地方傳遞到另一個地方。
實質上序列化機制是將類的值轉化為一個一般的(即連續的)字節流,然后就可以將該流寫到磁盤文件或任何其他流化目標上。而要想實際的寫出這個流,就要使用那些實現了IFormatter接口的類里的Serialize和Deserialize方法。 在.net框架里提供了這樣兩個類:
一、BinaryFormatter
BinaryFormatter使用二進制格式化程序進行序列化。您只需創建一個要使用的流和格式化程序的實例,然后調用格式化程序的 Serialize 方法。流和要序列化的對象實例作為參數提供給此調用。類中的所有成員變量(甚至標記為 private 的變量)都將被序列化。
首先我們創建一個類: [Serializable] public class MyObject { public int n1 = 0; public int n2 = 0; public String str = null; } Serializable屬性用來明確表示該類可以被序列化。同樣的,我們可以用NonSerializable屬性用來明確表示類不能被序列化。 接著我們創建一個該類的實例,然后序列化,并存到文件里持久: 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();
而將對象還原到它以前的狀態也非常容易。首先,創建格式化程序和流以進行讀取,然后讓格式化程序對對象進行反序列化。 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);
二、SoapFormatter
前面我們用BinaryFormatter以二進制格式來序列化。很容易的我們就能把前面的例子改為用SoapFormatter的,這樣將以xml格式化,因此能有更好的可移植性。所要做的更改只是將以上代碼中的格式化程序換成 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 { public int n3; }
然而關于格式化器,還有個問題,假設我們只需要xml,但不需要soap特有的額外信息,那么該怎么做?有兩個方案:1、編寫一個實現IFormatter接口的類,采用的方式類似于SoapFormatter,但是可以沒有你不需要的信息;2、使用框架提供的類XmlSerializer。 XmlSerializer類和前兩個主流的序列化類的幾個不同點是: 1、不需要Serializable屬性,Serializable和NonSerializable屬性將會被忽略,但是使用XmlIgnore屬性,和NonSerializable屬性類似。 2、該類不能安全地訪問私有變成員,所以學要將私有成員改為公共成員,或者提供合適的公共特性。 3、要求被序列化的類要有一個默認的構造器。
我們改一下前面的MyObject類為:
public class MyObject { public int n1; public String str; public MyObject(){} public MyObject(n1,str) { this.n1=n1; this.str=str; } public override string ToString() { return String.Format("{0}:{1}",this.str,this.n1); } }
現在我們用XmlSerializer類來對修改后的MyObject進行序列化。因為XmlSerializer類的構造器里有個Type參數,所以XmlSerializer對象被明確的 連到該Type參數所表示的類了。XmlSerializer類也有Serialize和Deserialize方法: MyObject obj = new MyObject(12,"some string..."); XmlSerializer formatter = new XmlSerializer(typeof(MyObject)); Stream stream = new FileStream("MyFile.xml", FileMode.Create, FileAccess.Write, FileShare.None); formatter.Serialize(stream, obj); //下面是反序列化 stream.Seek(0,SeekOrigin.Begin) MyObject obj_out=(MyObject)formatter.Deserialize(stream) stream.Close(); Console.WriteLine(obj_out);
這個簡單的列子可以加以擴展,以便利用更多的XmlSerializer功能,包括使用屬性控制xml標記、使用xml模式和進行soap編碼。
自定義序列化
如果你希望讓用戶對類實現序列化,但是對數據流的組織方式不完全滿意,那么可以通過在對象上實現 ISerializable 接口來自定義序列化過程。這一功能在反序列化后成員變量的值失效時尤其有用,但是需要為變量提供值以重建對象的完整狀態。除了必須將類申明為 Serializable 的同時,還要要實現 ISerializable接口,需要實現 GetObjectData 方法以及一個特殊的構造函數,在反序列化對象時要用到此構造函數。在實現 GetObjectData 方法時,最常調用的SerializationInfo的方法是AddValue,這個方法具有針對所有標準類型(int、char等等)的重載版本;而 StreamingContext 參數描述給定的序列化流的源和目標,這樣我們就可以知道我們是將對象序列化到持久性存儲還是在將他們跨進程或機器序列化。而在反序列化時,我們調用SerializationInfo提供的一組Getxxx方法,他們針對所有標準類型數據執行各種AddValue重載版本的逆操作。下代碼示例說明了如何在前一部分中提到的 MyObject 類上實現 ISerializable。
[Serializable] public class MyObject : ISerializable { public int n1; public int n2; public String str;
public MyObject() { }
protected MyObject(SerializationInfo info, StreamingContext context) { n1 = info.GetInt32("i"); n2 = info.GetInt32("j"); str = info.GetString("k"); }
public virtual 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相同,這個構造函數將會在反序列化的過程中使用:格式化器從流中反序列化數據,然后通過這個構造函數對對象進行實列化。如果缺少 GetObjectData,編譯器將發出警告。但是,由于無法強制實現構造函數,所以,缺少構造函數時不會發出警告。如果在沒有構造函數的情況下嘗試反序列化某個類,將會出現異常。在消除潛在安全性和版本控制問題等方面,當前設計優于 SetObjectData 方法。例如,如果將 SetObjectData 方法定義為某個接口的一部分,則此方法必須是公共方法,這使得用戶不得不編寫代碼來防止多次調用 SetObjectData 方法。可以想象,如果某個對象正在執行某些操作,而某個惡意應用程序卻調用此對象的 SetObjectData 方法,將會引起一些潛在的麻煩。
在反序列化過程中,使用出于此目的而提供的構造函數將 SerializationInfo 傳遞給類。對象反序列化時,對構造函數的任何可見性約束都將被忽略,因此,可以將類標記為 public、protected、internal 或 private。一個不錯的辦法是,在類未封裝的情況下,將構造函數標記為 protect。如果類已封裝,則應標記為 private。要還原對象的狀態,只需使用序列化時采用的名稱,從 SerializationInfo 中檢索變量的值。如果基類實現了 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 方法。此時,引用的所有子對象均已完全還原。有些類不使用上述事件偵聽器,很難對它們進行反序列化,散列表便是一個典型的例子。在反序列化過程中檢索關鍵字/值對非常容易,但是,由于無法保證從散列表派生出的類已反序列化,所以把這些對象添加回散列表時會出現一些問題。因此,建議目前不要在散列表上調用方法。
|