一、什麼是執行時序列化
序列化的作用就是將物件圖(特定時間點的物件連線圖)轉換為位元組流,這樣這些物件圖就可以在檔案系統/網路進行傳輸。
二、序列化/反序列化快速入門
一般來說我們通過 FCL 提供的 BinaryFormatter
物件就可以將一個物件序列化為位元組流進行儲存,或者通過該 Formatter 將一個位元組流反序列化為一個物件。
FCL 的序列化與反序列化
序列化操作:
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
。
在程式集載入完成之後,會在該程式集搜尋待反序列化的物件圖型別,找不到則會丟擲異常。
【注意】
某些應用程式通過
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 特性
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
資訊來取得每個欄位儲存的值陣列。該陣列與欄位資訊陣列是並行的,下標一致。 - 格式化器寫入型別的程式集等資訊。
- 遍歷兩個陣列,寫入欄位資訊與其資料到流當中。
反序列化操作的步驟與上面相反。
- 首先從流頭部讀取程式集標識與型別資訊,如果當前 AppDomain 沒有載入該程式集會丟擲異常。如果型別的程式集已經載入,則通過
FormatterServices.GetTypeFromAssembly()
方法來構造一個 Type 物件。 - 格式化器呼叫
FormatterService.GetUninitializedObject()
方法為新物件分配記憶體,但是 不會呼叫物件的構造器。 - 格式化器通過
FormatterService.GetSerializableMembers()
初始化一個MemberInfo
陣列。 - 格式化器根據流中的資料建立一個 Object 陣列,該陣列就是欄位的資料。
- 格式化器通過
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 方法轉換失敗的情況下才會使用上述機制。
子類與基類的 ISerializable
如果某個子類整合了基類,那麼子類在其 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()
方法,該方法是在序列化時被呼叫,會傳入他想要序列化的型別。