Dot-Net
.NET 與 BinaryFormatter 的向後兼容性
我們在 C# 遊戲中使用 BinaryFormatter 來保存使用者遊戲進度、遊戲關卡等。我們遇到了向後兼容的問題。
目的:
- 關卡設計師創建活動(關卡和規則),我們更改程式碼,活動應該仍然可以正常工作。在發布之前的開發過程中每天都會發生這種情況。
- 使用者保存遊戲,我們發布了遊戲更新檔,使用者應該仍然可以載入遊戲
- 無論兩個版本相距多遠,不可見的數據轉換過程都應該起作用。例如,使用者可以跳過我們的前 5 個小更新並直接獲得第 6 個。儘管如此,他保存的遊戲應該仍然可以正常載入。
該解決方案需要對使用者和關卡設計師完全不可見,並且最小化想要更改某些內容的編碼人員的負擔(例如,重命名欄位,因為他們認為更好的名稱)。
我們序列化的一些對像圖植根於一個類,一些則植根於其他類。不需要前向兼容性。
潛在的重大變化(以及當我們序列化舊版本並反序列化到新版本時會發生什麼):
- 添加欄位(獲取預設初始化)
- 更改欄位類型(失敗)
- 重命名欄位(相當於刪除它並添加一個新的)
- 將屬性更改為欄位並返回(相當於重命名)
- 更改自動實現的屬性以使用支持欄位(相當於重命名)
- 添加超類(相當於將其欄位添加到目前類)
- 以不同的方式解釋欄位(例如,以度為單位,現在以弧度為單位)
- 對於實現 ISerializable 的類型,我們可能會更改 ISerializable 方法的實現(例如,開始在 ISerializable 實現中對一些非常大的類型使用壓縮)
- 重命名一個類,重命名一個列舉值
我讀過:
- 版本容錯序列化
- IDeserializationCallback
- $$ OptionalField(VersionAdded) $$
- $$ OnDeserializing $$,$$ OnDeserialized $$,$$ OnSerializing $$,$$ OnSerialized $$.
- $$ NotSerialized $$
我目前的解決方案:
- 我們通過使用諸如 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 反序列化
- 獲取所有必須反序列化的欄位(用自己的屬性註釋)
- 用反序列化對像中的數據填充對象
適用於我們所有的數據,自三四個版本以來。