Dot-Net

.NET 與 BinaryFormatter 的向後兼容性

  • October 25, 2019

我們在 C# 遊戲中使用 BinaryFormatter 來保存使用者遊戲進度、遊戲關卡等。我們遇到了向後兼容的問題。

目的:

  • 關卡設計師創建活動(關卡和規則),我們更改程式碼,活動應該仍然可以正常工作。在發布之前的開發過程中每天都會發生這種情況。
  • 使用者保存遊戲,我們發布了遊戲更新檔,使用者應該仍然可以載入遊戲
  • 無論兩個版本相距多遠,不可見的數據轉換過程都應該起作用。例如,使用者可以跳過我們的前 5 個小更新並直接獲得第 6 個。儘管如此,他保存的遊戲應該仍然可以正常載入。

該解決方案需要對使用者和關卡設計師完全不可見,並且最小化想要更改某些內容的編碼人員的負擔(例如,重命名欄位,因為他們認為更好的名稱)。

我們序列化的一些對像圖植根於一個類,一些則植根於其他類。不需要前向兼容性。

潛在的重大變化(以及當我們序列化舊版本並反序列化到新版本時會發生什麼):

  • 添加欄位(獲取預設初始化)
  • 更改欄位類型(失敗)
  • 重命名欄位(相當於刪除它並添加一個新的)
  • 將屬性更改為欄位並返回(相當於重命名)
  • 更改自動實現的屬性以使用支持欄位(相當於重命名)
  • 添加超類(相當於將其欄位添加到目前類)
  • 以不同的方式解釋欄位(例如,以度為單位,現在以弧度為單位)
  • 對於實現 ISerializable 的類型,我們可能會更改 ISerializable 方法的實現(例如,開始在 ISerializable 實現中對一些非常大的類型使用壓縮)
  • 重命名一個類,重命名一個列舉值

我讀過:

我目前的解決方案

  • 我們通過使用諸如 OnDeserializing 回調之類的東西,盡可能多地進行非破壞性更改。
  • 我們每兩周安排一次重大更改,因此需要保留的兼容性程式碼更少。
  • 每次在進行重大更改之前,我們都會複製所有$$ Serializable $$我們使用的類放入名為 OldClassVersions.VersionX 的命名空間/文件夾中(其中 X 是最後一個序號之後的下一個序號)。即使我們不會很快發布,我們也會這樣做。
  • 當寫入文件時,我們序列化的是這個類的一個實例:class SaveFileData { int version; 對像數據;}
  • 從文件讀取時,我們反序列化 SaveFileData 並將其傳遞給執行如下操作的迭代“更新”常式:

.

for(int i = loadedData.version; i < CurrentVersion; i++)
{
   // Update() takes an instance of OldVersions.VersionX.TheClass
   // and returns an instance of OldVersions.VersionXPlus1.TheClass
   loadedData.data = Update(loadedData.data, i);
}
  • 為方便起見,Update() 函式在其實現中可以使用 CopyOverlappingPart() 函式,該函式使用反射將盡可能多的數據從舊版本複製到新版本。這樣,Update() 函式只能處理實際更改的內容。

一些問題:

  • 反序列化器反序列化為類 Foo 而不是類 OldClassVersions.Version5.Foo - 因為類 Foo 是被序列化的。
  • 幾乎不可能測試或調試
  • 需要保留很多類的舊副本,這容易出錯、脆弱和煩人
  • 當我們想重命名一個類時,我不知道該怎麼辦

這應該是一個非常普遍的問題。人們通常如何解決它?

艱難的一個。我會轉儲二進製文件並使用 XML 序列化(更易於管理,容忍不太極端的更改 - 例如添加/刪除欄位)。在更極端的情況下,更容易編寫從一個版本到另一個版本的轉換(也許是 xslt)並保持類乾淨。如果要求不透明度和小磁碟佔用空間,您可以嘗試在寫入磁碟之前壓縮數據。

我們在儲存使用者配置文件數據(網格列排列,過濾器設置……)的應用程序中遇到了同樣的問題。

在我們的例子中,問題是 AssemblyVersion。

對於這個問題,我創建了一個SerializationBinder讀取程序集的實際程序集版本(所有程序集在新部署中獲得新版本號) Assembly.GetExecutingAssembly().GetName().Version

在覆蓋方法BindToType中,類型資訊是使用新的程序集版本創建的。

反序列化是“手動”實現的,這意味著

  • 通過普通 BinaryFormatter 反序列化
  • 獲取所有必須反序列化的欄位(用自己的屬性註釋)
  • 用反序列化對像中的數據填充對象

適用於我們所有的數據,自三四個版本以來。

引用自:https://stackoverflow.com/questions/3583694