.NET与BinaryFormatter的向后兼容性

时间:2010-08-27 11:29:14

标签: .net backwards-compatibility binaryformatter

我们在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是序列化的。
  • 几乎无法测试或调试
  • 要求保留很多类的旧副本,这很容易出错,易碎且烦人。
  • 当我们想要重命名一个类
  • 时,我不知道该怎么做

这应该是一个非常普遍的问题。人们通常如何解决它?

3 个答案:

答案 0 :(得分:3)

艰难的一个。我会转储二进制文件并使用XML序列化(更容易管理,容忍不太极端的更改 - 比如添加/删除字段)。在更极端的情况下,更容易从一个版本到另一个版本编写转换(可能是xslt)并保持类清洁。如果要求不透明度和小磁盘占用,则可以在写入磁盘之前尝试压缩数据。

答案 1 :(得分:2)

我们的应用程序在存储用户配置文件数据时遇到了同样的问题(网格列排列,过滤器设置......)。

在我们的案例中,问题是AssemblyVersion。

对于这个问题,我创建了一个SerializationBinder来读取实际的程序集版本 程序集(所有程序集在新部署时获得新版本号) 与Assembly.GetExecutingAssembly().GetName().Version

在覆盖方法BindToType中,使用新的程序集版本创建类型信息。

反序列化是“手动”实现的,这意味着

  • 通过普通BinaryFormatter进行反序列化
  • 获取所有必须反序列化的字段(使用自己的属性注释)
  • 使用反序列化对象的数据填充对象

使用我们的所有数据以及三到四个版本。

答案 2 :(得分:0)

这是一个非常老的问题,但是无论如何它都需要一个最新的答案。我知道这有点偏离主题,所以请耐心等待。今天,在2019年:我建议那些碰巧在您的项目中某个合理可行阶段的人读到 的人,认真考虑使用Protobuf而不是BinaryFormatter。它具有二进制格式的大部分优点(但确实如此),但其缺点较少。

  • 它可以轻松地在不同的语言和技术堆栈(Java,.NET,C ++,Go,Python)之间工作
  • 它具有经过深思熟虑的策略,可以以某种方式处理重大更改(添加/删除字段等),这意味着软件的“版本 x ”要容易得多处理“版本 y ”生成的数据,反之亦然。是的,这确实是对的:您的应用程序的较旧版本将能够处理使用Protobuf .proto接口定义的较新版本序列化的数据。 (反序列化时,不存在的字段将被忽略。)

    通过比较,运行新版本的代码并反序列化旧数据时,数据中的“不存在”字段将设置为特定于类型的默认值。从这种意义上说,处理旧数据并不是“完全自动化”的,但是比使用Java和.NET等平台随附的默认二进制序列化库时,它们简单得多。

如果您喜欢非二进制格式,则JSON通常是合适的选择。对于RPC和此类情况,Protobuf更好,甚至在当今,Microsoft都正式提及/认可:Introduction to gRPC on ASP.NET Core。 (gRPC是基于Protobuf构建的技术堆栈)