我们在 C# 游戏中使用 BinaryFormatter 来保存用户游戏进度、游戏级别等。我们遇到了向后兼容性的问题。
目的:
- 关卡设计师创建活动(关卡和规则),我们更改代码,活动应该仍然可以正常工作。在发布之前的开发过程中,这种情况每天都可能发生。
- 用户保存游戏,我们发布游戏补丁,用户应该仍然能够加载游戏
- 无论两个版本相距多远,隐形数据转换过程都应该有效。例如,用户可以跳过前 5 个小更新并直接获取第 6 个更新。尽管如此,他保存的游戏应该仍然可以正常加载。
该解决方案需要对用户和关卡设计师完全不可见,并且尽量减少想要更改某些内容的编码人员的负担(例如,因为他们想到了更好的名称而重命名字段)。
我们序列化的一些对象图植根于一个类,有些则植根于其他类。不需要向前兼容性。
潜在的重大更改(以及当我们序列化旧版本并反序列化为新版本时会发生什么):
- 添加字段(默认初始化)
- 更改字段类型(失败)
- 重命名字段(相当于删除它并添加新字段)
- 将属性更改为字段并返回(相当于重命名)
- 更改自动实现的属性以使用支持字段(相当于重命名)
- 添加超类(相当于将其字段添加到当前类中)
- 以不同方式解释字段(例如,以前以度为单位,现在以弧度为单位)
- 对于实现 ISerialized 的类型,我们可能会更改 ISerialized 方法的实现(例如,对于某些非常大的类型,开始在 ISerialized 实现中使用压缩)
- 重命名类、重命名枚举值
我读过:
- 版本容错序列化
- ID反序列化回调
- [可选字段(版本已添加)]
- [OnDeserializing]、[OnDeserialized]、[OnSerializing]、[OnSerialized]。
- [未序列化]
我目前的解决方案:
- 通过使用 OnDeserializing 回调等内容,我们尽可能多地进行不间断的更改。
- 我们每两周安排一次重大更改,因此需要保留的兼容性代码较少。
- 每次在我们做出重大改变之前,我们都会复制all我们使用的 [Serialized] 类放入名为 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)并保持类干净会更容易。如果需要不透明度和较小的磁盘占用空间,您可以尝试在写入磁盘之前压缩数据。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)