【发布时间】:2011-04-04 18:20:47
【问题描述】:
我们在 C# 游戏中使用 BinaryFormatter 来保存用户游戏进度、游戏关卡等。我们遇到了向后兼容的问题。
目标:
- 关卡设计师创建了活动(关卡和规则),我们更改了代码,活动应该仍然可以正常工作。在发布前的开发过程中每天都会发生这种情况。
- 用户保存游戏,我们发布了游戏补丁,用户应该仍然可以加载游戏
- 无论两个版本相距多远,不可见的数据转换过程都应该有效。例如,用户可以跳过我们的前 5 个小更新并直接获得第 6 个。不过,他保存的游戏应该仍然可以正常加载。
解决方案需要对用户和关卡设计师完全不可见,并且尽量减少想要更改某些内容的编码人员的负担(例如,重命名字段,因为他们想出了更好的名称)。
我们序列化的一些对象图植根于一个类,一些则植根于其他类。不需要向前兼容。
潜在的重大变化(以及当我们序列化旧版本并反序列化到新版本时会发生什么):
- 添加字段(默认初始化)
- 更改字段类型(失败)
- 重命名字段(相当于删除它并添加一个新的)
- 将属性更改为字段并返回(相当于重命名)
- 将自动实现的属性更改为使用支持字段(相当于重命名)
- 添加超类(相当于将其字段添加到当前类)
- 以不同方式解释字段(例如,以前以度为单位,现在以弧度为单位)
- 对于实现 ISerializable 的类型,我们可能会更改 ISerializable 方法的实现(例如,开始在 ISerializable 实现中对一些非常大的类型使用压缩)
- 重命名一个类,重命名一个枚举值
我读过:
- Version Tolerant Serialization
- IDeserializationCallback
- [可选字段(版本添加)]
- [OnDeserializing]、[OnDeserialized]、[OnSerializing]、[OnSerialized]。
- [未序列化]
我目前的解决方案:
- 我们通过使用 OnDeserializing 回调之类的东西,尽可能多地进行非破坏性更改。
- 我们计划每 2 周进行一次重大更改,因此需要保留的兼容性代码更少。
- 每次在进行重大更改之前,我们都会将所有我们使用的 [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 序列化还是找到更好的方法来做到这一点? XML 序列化有一些限制,不适用于我的程序,因此我计划按照您的方法添加 K.Hoffmann 的一些补充内容。
-
@i8abug:我切换到 XML 序列化,是的。如果您告诉我困扰您的限制,我可能会告诉您解决它们的方法,如果我知道的话。
-
谢谢!对于初学者,我需要序列化私有成员和受保护成员以及通用字典。我尝试查看 DataContract 序列化,但我需要序列化未知的继承类(由其他开发人员编写),而这对于 DataContracts 是不可能的。二进制序列化似乎是唯一有效的方法。我还检查了 protobuf-net,但也遇到了一些限制。当您切换到 XML 序列化时,您是如何处理版本化的?您是否为每个 XML 版本创建了一个特定的解释器,或者您仍然按照上面描述的方式进行操作?
-
关于私有成员 - 是的,我也需要它,所以我使用 DataContractSerializer 对 XML 进行序列化。另外,我不想用 [DataMember] 属性标记我的类,所以我将我的类设为 [Serializable] 而不是 [DataContract](但仍然使用 DCS 对它们进行序列化。它确实有效)。但是,显然这在“未知继承类”的情况下会失败,除非您要求您的继承者在应用程序启动时将自己添加到您的序列化代码稍后作为“knownTypes”arg 传递给 DCS 的一些静态“List
globalKnownTypes”。想想看,你[...] -
[...] 可能会自动执行此操作:将此代码添加到基类的 ctor:“Foo.globalKnownTypes.Add(this.GetType());”。这样,当派生类被实例化时,它会自动将自己注册为“已知类型”(因为它调用了基类 ctor)。但我可能完全不合时宜——我猜 MS 并不打算让你这样做。关于版本控制:到目前为止,我最终从未实现过它。我所做的只是开始使用 XML,因此,将来,如果我真的需要做出重大更改,我可以手动编辑 XML。与此同时,我一直在使用 [OptionalField] 和 [OnDeserialized]。
标签: .net backwards-compatibility binaryformatter