標籤:
本章前面討論過,控制序列化和還原序列化過程的最佳方式就是使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized、NonSerialized、OptionalField等attribute。然而,在一些極少見的情況下,這些attribute不能提供你希望的全部控制。除此之外,格式化器在內部使用了反射,而反射的速度比較慢,這會增大序列化和還原序列化對象所化的時間。為了序列化/還原序列化的資料進行完全的控制,並避免使用反射,你的類型可實現ISerializable介面:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
這個介面只有一個方法,即GetObjectData,但是,實現這個介面的大多數類型還實現了一個特殊的構造器,稍後會詳細描述它。
重要提示:ISerializable介面大問題在於,一旦類型實現了它,所有衍生類別型都必須實現他,而且衍生類別型必須能保證調用基類的GetObjectData方法和特殊構造器。除此之外,一旦類型實現了該介面,變永遠不能刪除它,否則會失去與衍生類別型的相容性。Sealed類型實現了ISerializable介面總是可行的。
重要提示:ISerializable介面和特殊的構造器旨在由格式化器使用。然而,其他代碼可能調用GetObjectData,後者可能返回敏感的資料。另外,其他代碼可能構造一個對象,並傳入損壞的資料,因此,建議將以下attribute應用於GetObjectData方法和特殊指令
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true]
格式化器序列化一個對象圖時,會檢查每個對象。如果發現一個對象的類型實現了ISerializable介面,格式化器就會忽略所有定製的attribute,該為構造一個新的SerializationInfo對象,這個 對象包含了要實際為對象序列化的值的集合。
構造一個SerializationInfo對象時,格式化器要傳遞兩個參數:Type和IFormatterConverter。Type參數標識了要序列化的對象。為了唯一的標識一個類型,需要兩部分資訊:類型的字串名稱以及程式集的標識(包括程式集名稱、版本、語言文化和公開金鑰)。一個SerializationInfo物件建構好之後,會包含類型的全名(通過查詢Type的FullName屬性),並將這個額字串儲存到一個私人欄位中。為了擷取類型的全名可查詢SerializationInfo的FullTypeName屬性。類似的,構造器擷取類型的定義程式集,並將這個字串儲存到一個私人欄位中。為了擷取這個程式集標識,可查詢SerializationInfo的AssemblyName屬性。
構造好並初始化好SerializationInfo對象後,格式化器調用類型的GetObjectData方法,向它傳遞對SerializationInfo對象的引用。GetObjectData方法負責決定需要哪些資訊來序列化對象,並將這些資訊添加到SerializationInfo對象中。GetObjectData調用SerializationInfo類型提供的AddValue方法的眾多重載版本之一來指定要序列化的資訊。針對添加的每個資料,都要調用一次AddValue。
以下代碼展示了Dictionary<TKey,Tvalue>類型如何?ISerializable和IDeserializationCallback介面控制序列化和還原序列化:
[Serializable] public class Dictionary<TKey, TValue> : ISerializable, IDeserializationCallback { private SerializationInfo m_siInfo;//只用於反射 //用於控制還原序列化的特殊構造器 [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] protected Dictionary(SerializationInfo info, StreamingContext context) { //還原序列化期間,為OnDeserialization儲存SerializationInfo m_siInfo = info; } //用於控制序列化的方法 [SecurityCritical] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Version", m_version); info.AddValue("Comparer", m_comparer, typeof(IEqualityComparer<TKey>)); info.AddValue("HashSize", (m_buckets == null) ? 0 : m_buckets.Length); if (m_buckets != null) { KeyValuePair<TKey, TValue> array = new KeyValuePair<TKey, TValue>(count); CopyTo(array, 0); info.AddValue("KeyValuePairs", array, typeof(KeyValuePair<TKey, TValue>[])); } } //所有key/value對象都還原序列化好之後調用的方法 public virtual void IDeserializationCallback.OnDeserialization(Object sender) { if (m_siInfo == null) return; Int32 num = m_siInfo.GetInt32("Version"); Int32 num2 = m_siInfo.GetInt32("HashSize"); m_siInfo.GetValue("Comparer", typeof(IEqualityComparer<TKey>)); if (num2 != 0) { m_buckets = new Int32[num2]; for (Int32 i = 0; i < m_buckets.Length; i++) m_buckets[i] = -1; m_entries=new Entry<TKey,TValue>[num2]; m_freeList = -1; KeyValuePair<TKey, TValue>[] pairArray =(KeyValuePair<TKey, TValue>[]); m_siInfo.GetValue("KeyValuePairs", typeof(KeyValuePair<TKey, TValue> []));。。。。。。。。 } }
每個AddValue方法都回去一個String名稱和一些資料。資料一般是簡單的實值型別,比如Boolen,char,byte,sbyte,int16,uint32,int64,uint64,single,double,decimal,datetime.然而,還可以在調用AddValue時向它傳遞一個Object的引用。GetObjectData添加好所有的序列化資訊之後,會返回至格式化器。
現在,格式化器擷取已經添加到SerializationInfo對象的所有值,並把他們序列化到流中。注意,我們還向GetObjectData方法傳遞了另一個參數,也就是對一個StreamingContext對象的引用。大多數的GetObjectData都會忽略這個參數。後面講討論它。
知道了如何設定序列化所需的資訊之後,再來看看還原序列化。格式化器從流中讀取一個對象時,會為新對象分配記憶體(FormatterServices.GetUninitializedObject)。最初,這個對象的所有欄位都設為0或null。然後,格式化器檢查類型是否實現了ISerializable介面。如果存在這個介面,格式化器就嘗試調用一個特殊構造器,它的參數和GetObjectData方法完全一致。
如果你的類是密封類,強烈建議將這個特殊構造器聲明為private。這樣可防止任何代碼不慎調用它,從而提升安全性。如果不是密封類,應該將這個特殊構造器聲明為protect,確保之後衍生類別才能調用它。注意,無論這個特殊的建構函式時如何聲明的,格式化器都能調用它。
構造器擷取對一個SerializationInfo對象引用。在這個SerializationInfo對象中,包含了對象序列化時添加的所有值。特殊構造器可調用GetBoolean,GetChar,GetBtye,GetSByte,GetUInt16,GetInt32,GetUInt32,GetInt64,GetUInt64,GetSingle,GetDouble,GetDecimal,GetDateTime,GetString和GetValue等任何一個方法,向它傳遞與序列化一個值所用的名稱對用的一個字串。上述每個方法返回的值再用於初始化新對象的各個欄位。
還原序列化一個對象欄位時,應調用和對象序列化時傳遞給AddValue方法的值的類型匹配的一個Get方法。換言之,如果GetObjectData方法調用AddValue時傳遞的一個Int32值,那麼在還原序列化對象時,應該為同一個值調用GetInt32方法。如果值再流中的類型和你試圖擷取的類型不符合,格式化器會嘗試用一個IFormatterConverter對象將流中的值轉型成你指定的類型。
前面說過,構造SerializationInfo對象時,要向它傳遞類型實現了IFormatterConverter介面的一個對象。由于格式化器負責構造SerializationInfo對象,所以要有他選擇它想要的IFormatterConverter類型。微軟的 BinaryFormatter,SoapFormatter類型總是構造FormatterConverter類型的一個執行個體,微軟的格式化器沒有提供任何方法讓你選擇一個不同的IFormatterConverter類型。
FormatterConverter類型調用System.Convert類的各種靜態方法在不同的核心類型之間對值進行轉換,比如將一個Int32轉換成Int64。然而,為了在其他任何類型之間轉換一個值,FormatterConverter要調用Convert的ChangeType方法將序列化好的類型轉型為Iconvertible介面,在調用恰當的介面方法。所以,要允許一個可序列化類別型的對象還原序列化成一個不同的類型,可考慮讓自己的類型實現Iconvertible介面。注意,只有在還原序列化對象時調用一個Get方法,但發現它的類型和流中的值的類型不合符時才會使用FormatterConverter對象。
特殊構造器也可以不調用上面列出的各個Get方法,而是調用GetEnumerator。該方法會返回一個System.Runtime.Serialization.SerializationInfoEnumerator對象,可用該對象遍曆SerializationInfo對象中包含的所有值。枚舉的每個值都是一個System.Runtime.Serialization.SerializationEntry。
當然,你完全可以定義自己的一個類型,讓他從實現了ISerializable的GetObjectData方法和特殊構造器一個屬性派生。如果你的類型也實現了ISerializable,那麼在你的實現的GetObjectData方法和特殊的建構函式中,必須調用基類中的同名方法,確保對象能正確序列化和還原序列化,這一點務必記牢,否則對象是不能正確序列化和還原序列化的。
如果你的衍生類別中沒有任何額外的欄位,因而沒有特殊的序列化和還原序列化需求,就完全不必實現ISerializable。和所有介面成員相似,GetObjectData是virtual的,調用它可以正確的序列化對象。除此之外,格式化器將特殊的建構函式已虛擬化。換言之,還原序列化期間,格式化器會檢查要執行個體化的類型。如果那個類型沒有提供特殊構造器,格式化器就會掃描基類,知道它找到了實現特殊構造器的一個類。
重要提示:特殊構造器中的代碼一般會從傳給他的SerializationInfo對象中提取欄位。提取了欄位後,不能保證對象以完全序列化,所有特殊構造器中的代碼不應嘗試操縱它提取的對象。
如果你的類型必須訪問提取的一個對象中的成員(比如調用方法),建議你的類型提供一個應用了OnDeserialized的方法,或者讓你的類型實現了IDeserializationCallback.OnDeserialization。調用該方法時,所有對象的欄位都以設定好,然而,對於多個對象來說,他們額OnDeserialized或OnDeserialization方法的調用順序是沒有保障的。所以,雖然欄位可能已經初始化,但你仍然不知道被應用的對象是否已完全序列化好.
如何在基類沒有實現ISerializable的前提下定義一個實現他的類型
前面講過,ISerializable介面的功能非常強大,它允許一個類型完全控制如何對類型的執行個體進行序列化和還原序列化。然而,這個功能是由代價的;現在,該類型還是負責它的基底類型的所有欄位的序列化。如果基底類型也實現了ISerializable介面,那麼對基底類型的欄位進行序列化時很容易的。只需要調用基底類型的GetObjectData方法即可。
但有朝一日,你可能要定義一個類型來控制它的序列化,但它的基類沒有實現ISerializable介面,在這種情況下,衍生類別必須手動序列化基類的欄位,具體的做法是擷取他們的值,並把這些值添加到SerializationInfo集合中。然後,在你的特殊構造器中還必須從結合中取出這些值,並以某種方式設定基類的欄位。如果基類的欄位時public或protected欄位,那麼這一切都很容易實現。但是,如果基類欄位時private欄位,就很難或根本不可能實現。
下面代碼示範了正確實現ISerializable介面的GetObjectData方法和它的隱含的構造器,使基類的欄位被序列化:
[Serializable] internal class Base { protected String m_name = "Jeff"; public Base() { } } [Serializable] internal class Derived : Base, ISerializable { private DateTime m_date = DateTime.Now; public Derived() { } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] private Derived(SerializationInfo info, StreamingContext context) { //為我們的類和基類擷取可序列化的成員集合 Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); //從info對象還原序列化基類的欄位 for (Int32 i = 0; i < mi.Length; i++) { FieldInfo fi = (FieldInfo)mi[i]; fi.SetValue(this, info.GetValue(baseType.FullName + "+" + fi.Name, fi.FieldType)); } m_date = info.GetDateTime("Date"); } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { //為這個類型希望序列化的值 info.AddValue("Date", m_date); Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); for (Int32 i = 0; i < mi.Length; i++) { info.AddValue(baseType.FullName + "+" + mi[i].Name, ((FieldInfo)mi[i]).GetValue(this)); } } public override string ToString() { return String.Format("Name={0},Date={1}", m_name, m_date); } }
在上述代碼中,有一個名為Base的基類,它只用Serializable定製的attribute進行了標識。從Base派生的是Derived類,它出了應用Serializable標識,還實現了ISerializable介面。為了使局面變的更加有趣,兩個類都定義了m_name的一個String欄位。調用SerializationInfo的AddValue方法時,不能添加多個同名的值。在上述代碼中,解決這個問題的方案實在欄位名前附加其類名作為首碼,從而對每個欄位進行標識。例如,當GetObjectData方法調用AddValue來序列化Base的m_name欄位時,寫入的值的名稱是Base+m_name。
第五節:控制序列化和還原序列化的資料