我们在 C# 游戏中使用 BinaryFormatter 来保存用户游戏进度、游戏关卡等。我们遇到了向后兼容的问题。
目的:
- 关卡设计师创建活动(关卡和规则),我们更改代码,活动应该仍然可以正常工作。在发布之前的开发过程中每天都会发生这种情况。
- 用户保存游戏,我们发布了游戏补丁,用户应该仍然可以加载游戏
- 无论两个版本相距多远,不可见的数据转换过程都应该有效。例如,用户可以跳过我们的前 5 个小更新并直接获得第 6 个。尽管如此,他保存的游戏应该仍然可以正常加载。
该解决方案需要对用户和关卡设计师完全不可见,并且最小化想要更改某些内容的编码人员的负担(例如,重命名字段,因为他们想到了更好的名称)。
我们序列化的一些对象图植根于一个类,一些则植根于其他类。不需要前向兼容性。
潜在的重大变化(以及当我们序列化旧版本并反序列化到新版本时会发生什么):
- 添加字段(获取默认初始化)
- 更改字段类型(失败)
- 重命名字段(相当于删除它并添加一个新的)
- 将属性更改为字段并返回(相当于重命名)
- 更改自动实现的属性以使用支持字段(相当于重命名)
- 添加超类(相当于将其字段添加到当前类)
- 以不同的方式解释字段(例如,以度为单位,现在以弧度为单位)
- 对于实现 ISerializable 的类型,我们可能会更改 ISerializable 方法的实现(例如,开始在 ISerializable 实现中对一些非常大的类型使用压缩)
- 重命名一个类,重命名一个枚举值
我读过:
- 版本容错序列化
- IDeserializationCallback
- [可选字段(添加版本)]
- [OnDeserializing]、[OnDeserialized]、[OnSerializing]、[OnSerialized]。
- [未序列化]
我目前的解决方案:
- 我们通过使用诸如 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 是被序列化的。
- 几乎不可能测试或调试
- 需要保留很多类的旧副本,这容易出错、脆弱和烦人
- 当我们想重命名一个类时,我不知道该怎么办
这应该是一个非常普遍的问题。人们通常如何解决它?