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

来源:https://www.cnblogs.com/myzony/archive/2018/11/04/9905717.html
-Advertisement-
Play Games

一、什麼是運行時序列化 序列化的作用就是將對象圖(特定時間點的對象連接圖)轉換為位元組流,這樣這些對象圖就可以在文件系統/網路進行傳輸。 二、序列化/反序列化快速入門 一般來說我們通過 FCL 提供的 對象就可以將一個對象序列化為位元組流進行存儲,或者通過該 Formatter 將一個位元組流反序列化為一 ...


一、什麼是運行時序列化

序列化的作用就是將對象圖(特定時間點的對象連接圖)轉換為位元組流,這樣這些對象圖就可以在文件系統/網路進行傳輸。

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

一般來說我們通過 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 簽名一樣的委托,並且在反序列化註冊到當前 AppDomainAssemblyResolve 事件。

這樣當程式集載入失敗的時候,你可以在該方法內部根據傳入的事件參數與程式集標識自己使用 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。

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

通過 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 添加到我們新加的欄位之上,這樣的話在反序列化數據時就不會因為缺少欄位而拋出異常。

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

格式化器的核心就是 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() 方法,傳入新分配的對象、欄位信息數組、欄位數據數組進行對象初始化。

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

一般來說通過在第四節說的那些特性控制就已經滿足了大部分需求,但格式化器內部使用的是反射,反射性能開銷比較大,如果你想要針對序列化/反序列化進行完全的控制,那麼你可以實現 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 來手動針對基類的欄位進行賦值。

七、流上下文

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

八、類型序列化為不同的類型與對象反序列化為不同的對象

在某些時候可能需要更改序列化完成之後的對象類型,這個時候只需要對象在其實現 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 進行回收處理。

九、序列化代理

當某些時候需要對一個第三方庫對象進行序列化的時候,沒有其源碼,但是想要進行序列化,則可以通過序列化代理來進行序列化操作。

要實現序列化代理,需要實現 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 類型綁定不同用途的代理類。

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

通過繼承 SerializationBinder 抽象類,我們可以很方便地實現類型反序列化時轉化為不同的類型,該抽象類有一個 Type BindToType(String assemblyName,String typeName) 方法。

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

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

【註意】

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用BeautyEye L&F 漂亮外觀 ...
  • 配置PHP開發環境的時候,當進行到在Apache的httpd.conf文件中配置載入PHP模塊時發生如下錯誤 httpd: Syntax error on line 185 of D:/wamp/Apache24/conf/httpd.conf: Cannot load D:/wamp/php 5. ...
  • Like music and movies, video games are rapidly becoming an integral part of our lives. Over the years, youve yearned for every new gaming console, mas ...
  • 1.arrary_merge 示例代碼: 運行上面的代碼,輸出結果如下圖所示: 普通數組合併時,會把第二個數組放到第一個數組後面,拼接後返回。 但是對於鍵值對的數組來說,如果有相同的鍵,那麼第二個數組會覆蓋第一個數組相同的鍵所對應的值。 2.通過 合併 示例代碼: 運行上面的代碼,輸出結果如下圖所示 ...
  • datetimepicker造成的問題,年、月和日參數描述無法表示的 DateTime ...
  • 原則的誕生:面向對象:封裝、繼承、多態三大支柱蘊含了用抽象來封裝變化,降低耦合,實現復用的精髓; 封裝:隱藏內部的實現,保護內部信息; 繼承:實現復用,歸納共性; 多態:改寫對象行為,實現更高級別的繼承 要實現這些目的,就必須遵守一些原則:封裝變化、對介面編程、少繼承多聚合 實現系統的可擴展、可復用 ...
  • FormsAuthentication.SetAuthCookie(UserFlag, createPersistentCookie); createPersistentCookie是否永久保存cookie https://www.cnblogs.com/joeylee/p/3521131.html ...
  • 一、線程開銷 操作系統創建線程是有代價的,其主要開銷在下麵列舉出來了。 記憶體開銷 1. 線程內核對象 擁有線程描述屬性與線程上下文,線程上下文占用的記憶體空間為 x86 架構 占用 700 位元組、x64 架構 1240 位元組 、ARM 架構 350 位元組。 2. 線程環境塊(TEB) TEB 消耗一個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...