序列化的做用就是將對象圖(特定時間點的對象鏈接圖)轉換爲字節流,這樣這些對象圖就能夠在文件系統/網絡進行傳輸。數組
通常來講咱們經過 FCL 提供的 BinaryFormatter
對象就能夠將一個對象序列化爲字節流進行存儲,或者經過該 Formatter 將一個字節流反序列化爲一個對象。安全
序列化操做:網絡
public MemoryStream SerializeObj(object sourceObj) { var memStream = new MemoryStream(); var formatter = new BinaryFormatter(); formatter.Serialize(memStream, sourceObj); return memStream; }
反序列化操做:函數
public object DeserializeFromStream(MemoryStream stream) { var formatter = new BinaryFormatter(); stream.Position = 0; return formatter.Deserialize(stream); }
反序列化經過 Formatter 的
Deserialize()
方法返回序列化好的對象圖的根對象的一個引用。工具
經過序列化與反序列化的特性,能夠實現一個深拷貝的方法,用戶建立源對象的一個克隆體。性能
public object DeepClone(object originalObj) { using (var memoryStream = new MemoryStream()) { var formatter = new BinaryFormatter(); formatter.Serialize(memoryStream, originalObj); // 代表對象是被克隆的,能夠安全的訪問其餘託管資源 formatter.Context = new StreamingContext(StreamingContextStates.Clone); memoryStream.Position = 0; return formatter.Deserialize(memoryStream); } }
另一種技巧就是能夠將多個對象圖序列化到一個流當中,即調用屢次 Serialize()
方法將多個對象圖序列化到流當中。若是須要反序列化的時候,按照序列化時對象圖的序列化順序反向反序列化便可。設計
BinaryFormatter
在序列化的時候會將類型的全名與程序集定義寫入到流當中,這樣在反序列化的時候,格式化器會獲取這些信息,而且經過 System.Reflection.Assembly.Load()
方法將程序集加載到當前的 AppDomain
。代理
在程序集加載完成以後,會在該程序集搜索待反序列化的對象圖類型,找不到則會拋出異常。code
【注意】orm
某些應用程序經過
Assembly.LoadFrom()
來加載程序集,而後根據程序集中的類型來構造對象。序列化該對象是沒問題的,可是反序列化的時候格式化器使用的是Assembly.Load()
方法來加載程序集,這樣的話就會致使沒法正確加載對象。這個時候,你能夠實現一個與
System.ResolveEventHandler
簽名同樣的委託,而且在反序列化註冊到當前AppDomain
的AssemblyResolve
事件。這樣當程序集加載失敗的時候,你能夠在該方法內部根據傳入的事件參數與程序集標識本身使用
Assembly.LoadFrom()
來構造一個Assembly
對象。記得在反序列化完成以後,立刻向事件註銷這個方法,不然會形成內存泄漏。
在設計自定義類型時,你須要顯式地經過 Serializable
特性來聲明你的類型是能夠被序列化的。若是沒有這麼作,在使用格式化器進行序列化的時候,則會拋出異常。
[Serializable] public class DIYClass { public int x { get; set; } public int y { get; set; } }
【注意】
正由於這樣,咱們通常都會現將結果保存到
MemoryStream
之中,當沒有拋出異常以後再將這些數據寫入到文件/網絡。
Serializable
特性只能用於值類型、引用類型、枚舉類型(默認)、委託類型(默認),並且是不可被子類繼承。
若是有一個 A 類與其派生類 B 類,那麼 A 類沒擁有 Serializable
特性,而子類擁有,同樣的是沒法進行序列化操做。
並且序列化的時候,是將全部訪問級別的字段成員都進行了序列化,包括 private 級別成員。
能夠經過 System.NonSerializedAttribute
特性來確保某個字段在序列化時不被處理其值,例以下列代碼:
[Serializable] public class DIYClass { public DIYClass() { x = 10; y = 100; z = 1000; } public int x { get; set; } public int y { get; set; } [NonSerialized] public int z; }
在序列化以前,該自定義對象 z 字段的值爲 1000,在序列化時,檢測到了忽略特性,則不會寫入該字段的值到流當中。而且在反序列化以後,z 的值爲 0,而 x ,y 的值是 10 和 100。
經過 OnSerializing
、OnSerialized
、OnDeserializing
、OnDeserialized
這四個特性,咱們能夠在對象序列化與反序列化時進行一些自定義的控制。只須要將這四個特性分別加在四個方法上面便可,可是針對方法簽名必須返回值爲 void,同時也須要用有一個 StreamingContext
參數。
並且通常建議將這四個方法標識爲 private ,防止其餘對象誤調用。
[Serializable] public class DIYClass { [OnDeserializing] private void OnDeserializing(StreamingContext context) { Console.WriteLine("反序列化的時候,會調用本方法."); } [OnDeserialized] private void OnDeserialized(StreamingContext context) { Console.WriteLine("反序列化完成的時候,會調用本方法."); } [OnSerializing] public void OnSerializing(StreamingContext context) { Console.WriteLine("序列化的時候,會調用本方法."); } [OnSerialized] public void OnSerialized(StreamingContext context) { Console.WriteLine("序列化完成的時候,會調用本方法."); } }
【注意】
若是 A 類型有兩個版本,第 1 個版本有 5 個字段,並被序列化存儲到了文件當中。後面因爲業務須要,針對於 A 類型增長了 2 個新的字段,這個時候若是從文件中讀取第 1 個版本的對象流信息,就會拋出異常。
咱們能夠經過
System.Runtime.Serialization.OptionalFieldAttribute
添加到咱們新加的字段之上,這樣的話在反序列化數據時就不會由於缺乏字段而拋出異常。
格式化器的核心就是 FCL 提供的 FormatterServices
的靜態工具類,下列步驟體現了序列化器如何結合 FormatterServices
工具類來進行序列化操做的。
FormatterService.GetSerializableMembers()
方法得到須要序列化的字段構成的 MemberInfo
數組。FormatterService.GetObjectData()
方法,經過以前獲取的字段 MethodInfo
信息來取得每一個字段存儲的值數組。該數組與字段信息數組是並行的,下標一致。反序列化操做的步驟與上面相反。
FormatterServices.GetTypeFromAssembly()
方法來構造一個 Type 對象。FormatterService.GetUninitializedObject()
方法爲新對象分配內存,可是 不會調用對象的構造器。FormatterService.GetSerializableMembers()
初始化一個 MemberInfo
數組。FormatterService.PopulateObjectMembers()
方法,傳入新分配的對象、字段信息數組、字段數據數組進行對象初始化。通常來講經過在第四節說的那些特性控制就已經知足了大部分需求,但格式化器內部使用的是反射,反射性能開銷比較大,若是你想要針對序列化/反序列化進行徹底的控制,那麼你能夠實現 ISerializable
接口來進行控制。
該接口只提供了一個 GetObjectData()
方法,原型以下:
public interface ISerializable{ void GetObjectData(SerializationInfo info,StreamingContext context); }
【注意】
使用了
ISerializable
接口的代價就是其集成類都必須實現它,並且還要保證子類必須調用基類的GetObjectData()
方法與其構造函數。通常來講密封類才使用ISerializable
,其餘的類型使用特性控制便可知足。另外爲了防止其餘的代碼調用
GetObjectData()
方法,能夠經過一下特性來防止誤操做:[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
若是格式化器檢測到了類型實現了該接口,則會忽略掉原有的特性,而且將字段值傳入到 SerializationInfo
之中。
經過這個 Info 咱們能夠被序列化的類型,由於 Info 提供了 FullTypeName
與 AssemblyName
,不過通常推薦使用該對象提供的 SetType(Type type)
方法來進行操做。
格式化器構造完成 Info 以後,則會調用 GetObjectData()
方法,這個時候將以前構造好的 Info 傳入,而該方法則決定須要用哪些數據來序列化對象。這個時候咱們就能夠經過 Info 的 AddValue()
方法來添加一些信息用於反序列化時使用。
在反序列化的時候,須要類型提供一個特殊的構造函數,對於密封類來講,該構造函數推薦爲 private ,而通常的類型推薦爲 protected,這個特殊的構造函數方法簽名與 GetObjectData()
同樣。
由於在反序列化的時候,格式化器會調用這個特殊的構造函數。
如下代碼就是一個簡單實踐:
public class DIYClass : ISerializable { public int X { get; set; } public int Y { get; set; } public DIYClass() { } protected DIYClass(SerializationInfo info, StreamingContext context) { X = info.GetInt32("X"); Y = 20; } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("X", 10); } }
該類型的對象在反序列化以後,X 的值爲序列化以前的值,而 Y 的值始終都會爲 20。
【注意】
若是你存儲的 X 值是 Int32 ,而在獲取的時候是經過 GetInt64() 進行獲取。那麼格式化器就會嘗試使用
System.Convert
提供的方法進行轉換,而且能夠經過實現IConvertible
接口來自定義本身的轉換。不過只有在 Get 方法轉換失敗的狀況下才會使用上述機制。
若是某個子類集成了基類,那麼子類在其 GetObjectData()
與特殊構造器中都要調用父類的方法,這樣纔可以完成正確的序列化/反序列化操做。
若是基類沒有實現 ISerializable
接口與特殊的構造器,那麼子類就須要經過 FormatterService
來手動針對基類的字段進行賦值。
流上下文 StreamingContext
只有兩個屬性,第一個是狀態標識位,用於標識序列化/反序列化對象的來源與目的地。而第二個屬性就是一個 Object 引用,該引用則是一個附加的上下文信息,由用戶進行提供。
在某些時候可能須要更改序列化完成以後的對象類型,這個時候只須要對象在其實現 ISerializable
接口的 GetObjectData()
方法內部經過 SerializationInfo
的 SetType()
方法變動了序列化的目標類型。
下面的代碼演示瞭如何序列化一個單例對象:
[Serializable] public sealed class Singleton : ISerializable { private static readonly Singleton _instance = new Singleton(); private Singleton() { } public static Singleton GetSingleton() { return _instance; } [SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter =true)] void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SingletonHelper)); } }
這裏經過顯式實現接口的 GetObjectData()
方法來將序列化的目標類型設置爲 SingletonHelper
,該類型的定義以下:
[Serializable] public class SingletonHelper : IObjectReference { public object GetRealObject(StreamingContext context) { return Singleton.GetSingleton(); } }
這裏由於 SingletonHelper
實現了 IObjectReference
接口,當格式化器嘗試進行反序列化的時候,因爲在 GetObjectData()
欺騙了轉換器,所以反序列化的時候檢測到類型有實現該接口,因此會嘗試調用其 GetRealObject()
方法來進行反序列化操做。
而以上動做完成以後,SingletonHelper
會當即變爲不可達對象,等待 GC 進行回收處理。
當某些時候須要對一個第三方庫對象進行序列化的時候,沒有其源碼,可是想要進行序列化,則能夠經過序列化代理來進行序列化操做。
要實現序列化代理,須要實現 ISerializationSurrogate
接口,該接口擁有兩個方法,其簽名分別以下:
void GetObjectData(Object obj,SerializationInfo info,StreamingContext context); void SetObjectData(Object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector);
GetObjectData()
方法會在對象序列化時進行調用,而 SetObjectData()
會在對象反序列化時調用。
好比說咱們有一個需求是但願 DateTime
類型在序列化的時候經過 UTC 時間序列化到流中,而在反序列化時則更改成本地時間。
這個時候咱們就能夠本身實現一個序列化代理類 UTCToLocalTimeSerializationSurrogate
:
public sealed class UTCToLocalTimeSerializationSurrogate : ISerializationSurrogate { public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) { info.AddValue("Date", ((DateTime)obj).ToUniversalTime().ToString("u")); } public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { return DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime(); } }
而且在使用的時候,經過構造一個 SurrogateSelector
代理選擇器,傳入咱們針對於 DateTime
類型的代理,而且將格式化器與代理選擇器相綁定。那麼在使用格式化器的時候,就會經過咱們的代理類來處理 DateTime
類型對象的序列化/反序列化操做了。
static void Main(string[] args) { using (var stream = new MemoryStream()) { var formatter = new BinaryFormatter(); // 建立一個代理選擇器 var ss = new SurrogateSelector(); // 告訴代理選擇器,針對於 DateTime 類型採用 UTCToLocal 代理類進行序列化/反序列化代理 ss.AddSurrogate(typeof(DateTime), formatter.Context, new UTCToLocalTimeSerializationSurrogate()); // 綁定代理選擇器 formatter.SurrogateSelector = ss; formatter.Serialize(stream,DateTime.Now); stream.Position = 0; var oldValue = new StreamReader(stream).ReadToEnd(); stream.Position = 0; var newValue = (DateTime)formatter.Deserialize(stream); Console.WriteLine(oldValue); Console.WriteLine(newValue); } Console.ReadLine(); }
而一個代理選擇器容許綁定多個代理類,選擇器內部維護一個哈希表,經過 Type
與 StreamingContext
做爲其鍵來進行搜索,經過 StreamintContext
地不一樣能夠方便地爲 DateTime
類型綁定不一樣用途的代理類。
經過繼承 SerializationBinder
抽象類,咱們能夠很方便地實現類型反序列化時轉化爲不一樣的類型,該抽象類有一個 Type BindToType(String assemblyName,String typeName)
方法。
重寫該方法你就能夠在對象反序列化時,經過傳入的兩個參數來構造本身須要返回的真實類型。第一個參數是程序集名稱,第二個參數是格式化器想要反序列化時轉換的類型。
編寫好 Binder 類重寫該方法以後,在格式化器的 Binder
屬性當中綁定你的 Binder 類便可。
【注意】
抽象類還有一個
BindToName()
方法,該方法是在序列化時被調用,會傳入他想要序列化的類型。