《CLR Via C#》讀書筆記:24.運行時序列化

1、什麼是運行時序列化

序列化的做用就是將對象圖(特定時間點的對象鏈接圖)轉換爲字節流,這樣這些對象圖就能夠在文件系統/網絡進行傳輸。數組

2、序列化/反序列化快速入門

通常來講咱們經過 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代理

在程序集加載完成以後,會在該程序集搜索待反序列化的對象圖類型,找不到則會拋出異常。code

【注意】orm

某些應用程序經過 Assembly.LoadFrom() 來加載程序集,而後根據程序集中的類型來構造對象。序列化該對象是沒問題的,可是反序列化的時候格式化器使用的是 Assembly.Load() 方法來加載程序集,這樣的話就會致使沒法正確加載對象。

這個時候,你能夠實現一個與 System.ResolveEventHandler 簽名同樣的委託,而且在反序列化註冊到當前 AppDomainAssemblyResolve 事件。

這樣當程序集加載失敗的時候,你能夠在該方法內部根據傳入的事件參數與程序集標識本身使用 Assembly.LoadFrom() 來構造一個 Assembly 對象。

記得在反序列化完成以後,立刻向事件註銷這個方法,不然會形成內存泄漏。

3、使類型可序列化

在設計自定義類型時,你須要顯式地經過 Serializable 特性來聲明你的類型是能夠被序列化的。若是沒有這麼作,在使用格式化器進行序列化的時候,則會拋出異常。

[Serializable]
public class DIYClass
{
    public int x { get; set; }
    public int y { get; set; }
}

【注意】

正由於這樣,咱們通常都會現將結果保存到 MemoryStream 之中,當沒有拋出異常以後再將這些數據寫入到文件/網絡。

Serializable 特性

Serializable 特性只能用於值類型、引用類型、枚舉類型(默認)、委託類型(默認),並且是不可被子類繼承。

若是有一個 A 類與其派生類 B 類,那麼 A 類沒擁有 Serializable 特性,而子類擁有,同樣的是沒法進行序列化操做。

並且序列化的時候,是將全部訪問級別的字段成員都進行了序列化,包括 private 級別成員。

4、簡單控制序列化操做

禁止序列化某個字段

能夠經過 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。

序列化與反序列化的四個生命週期特性

經過 OnSerializingOnSerializedOnDeserializingOnDeserialized 這四個特性,咱們能夠在對象序列化與反序列化時進行一些自定義的控制。只須要將這四個特性分別加在四個方法上面便可,可是針對方法簽名必須返回值爲 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 添加到咱們新加的字段之上,這樣的話在反序列化數據時就不會由於缺乏字段而拋出異常。

5、格式化器的序列化原理

格式化器的核心就是 FCL 提供的 FormatterServices 的靜態工具類,下列步驟體現了序列化器如何結合 FormatterServices 工具類來進行序列化操做的。

  1. 格式化器調用 FormatterService.GetSerializableMembers() 方法得到須要序列化的字段構成的 MemberInfo 數組。
  2. 格式化器調用 FormatterService.GetObjectData() 方法,經過以前獲取的字段 MethodInfo 信息來取得每一個字段存儲的值數組。該數組與字段信息數組是並行的,下標一致。
  3. 格式化器寫入類型的程序集等信息。
  4. 遍歷兩個數組,寫入字段信息與其數據到流當中。

反序列化操做的步驟與上面相反。

  1. 首先從流頭部讀取程序集標識與類型信息,若是當前 AppDomain 沒有加載該程序集會拋出異常。若是類型的程序集已經加載,則經過 FormatterServices.GetTypeFromAssembly() 方法來構造一個 Type 對象。
  2. 格式化器調用 FormatterService.GetUninitializedObject() 方法爲新對象分配內存,可是 不會調用對象的構造器
  3. 格式化器經過 FormatterService.GetSerializableMembers() 初始化一個 MemberInfo 數組。
  4. 格式化器根據流中的數據建立一個 Object 數組,該數組就是字段的數據。
  5. 格式化器經過 FormatterService.PopulateObjectMembers() 方法,傳入新分配的對象、字段信息數組、字段數據數組進行對象初始化。

6、控制序列化/反序列化的數據

通常來講經過在第四節說的那些特性控制就已經知足了大部分需求,但格式化器內部使用的是反射,反射性能開銷比較大,若是你想要針對序列化/反序列化進行徹底的控制,那麼你能夠實現 ISerializable 接口來進行控制。

該接口只提供了一個 GetObjectData() 方法,原型以下:

public interface ISerializable{
    void GetObjectData(SerializationInfo info,StreamingContext context);
}

【注意】

使用了 ISerializable 接口的代價就是其集成類都必須實現它,並且還要保證子類必須調用基類的 GetObjectData() 方法與其構造函數。通常來講密封類才使用 ISerializable ,其餘的類型使用特性控制便可知足。

另外爲了防止其餘的代碼調用 GetObjectData() 方法,能夠經過一下特性來防止誤操做:

[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]

若是格式化器檢測到了類型實現了該接口,則會忽略掉原有的特性,而且將字段值傳入到 SerializationInfo 之中。

經過這個 Info 咱們能夠被序列化的類型,由於 Info 提供了 FullTypeNameAssemblyName,不過通常推薦使用該對象提供的 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 來手動針對基類的字段進行賦值。

7、流上下文

流上下文 StreamingContext 只有兩個屬性,第一個是狀態標識位,用於標識序列化/反序列化對象的來源與目的地。而第二個屬性就是一個 Object 引用,該引用則是一個附加的上下文信息,由用戶進行提供。

8、類型序列化爲不一樣的類型與對象反序列化爲不一樣的對象

在某些時候可能須要更改序列化完成以後的對象類型,這個時候只須要對象在其實現 ISerializable 接口的 GetObjectData() 方法內部經過 SerializationInfoSetType() 方法變動了序列化的目標類型。

下面的代碼演示瞭如何序列化一個單例對象:

[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 進行回收處理。

9、序列化代理

當某些時候須要對一個第三方庫對象進行序列化的時候,沒有其源碼,可是想要進行序列化,則能夠經過序列化代理來進行序列化操做。

要實現序列化代理,須要實現 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();
}

而一個代理選擇器容許綁定多個代理類,選擇器內部維護一個哈希表,經過 TypeStreamingContext 做爲其鍵來進行搜索,經過 StreamintContext 地不一樣能夠方便地爲 DateTime 類型綁定不一樣用途的代理類。

10、反序列化對象時重寫程序集/類型

經過繼承 SerializationBinder 抽象類,咱們能夠很方便地實現類型反序列化時轉化爲不一樣的類型,該抽象類有一個 Type BindToType(String assemblyName,String typeName) 方法。

重寫該方法你就能夠在對象反序列化時,經過傳入的兩個參數來構造本身須要返回的真實類型。第一個參數是程序集名稱,第二個參數是格式化器想要反序列化時轉換的類型。

編寫好 Binder 類重寫該方法以後,在格式化器的 Binder 屬性當中綁定你的 Binder 類便可。

【注意】

抽象類還有一個 BindToName() 方法,該方法是在序列化時被調用,會傳入他想要序列化的類型。

相關文章
相關標籤/搜索