【问题标题】:Backwards compatibility in .NET with BinaryFormatter.NET 与 BinaryFormatter 的向后兼容性
【发布时间】:2011-04-04 18:20:47
【问题描述】:

我们在 C# 游戏中使用 BinaryFormatter 来保存用户游戏进度、游戏关卡等。我们遇到了向后兼容的问题。

目标:

  • 关卡设计师创建了活动(关卡和规则),我们更改了代码,活动应该仍然可以正常工作。在发布前的开发过程中每天都会发生这种情况。
  • 用户保存游戏,我们发布了游戏补丁,用户应该仍然可以加载游戏
  • 无论两个版本相距多远,不可见的数据转换过程都应该有效。例如,用户可以跳过我们的前 5 个小更新并直接获得第 6 个。不过,他保存的游戏应该仍然可以正常加载。

解决方案需要对用户和关卡设计师完全不可见,并且尽量减少想要更改某些内容的编码人员的负担(例如,重命名字段,因为他们想出了更好的名称)。

我们序列化的一些对象图植根于一个类,一些则植根于其他类。不需要向前兼容。

潜在的重大变化(以及当我们序列化旧版本并反序列化到新版本时会发生什么):

  • 添加字段(默认初始化)
  • 更改字段类型(失败)
  • 重命名字段(相当于删除它并添加一个新的)
  • 将属性更改为字段并返回(相当于重命名)
  • 将自动实现的属性更改为使用支持字段(相当于重命名)
  • 添加超类(相当于将其字段添加到当前类)
  • 以不同方式解释字段(例如,以前以度为单位,现在以弧度为单位)
  • 对于实现 ISerializable 的类型,我们可能会更改 ISerializable 方法的实现(例如,开始在 ISerializable 实现中对一些非常大的类型使用压缩)
  • 重命名一个类,重命名一个枚举值

我读过:

我目前的解决方案

  • 我们通过使用 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


【解决方案1】:

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

【讨论】:

  • BinarySerialization 版本容错的小改动,包括添加/删除字段。 “更易于管理”究竟是什么意思?实际上,XSLT 听起来像是一个很好的解决方案。不,文件大小和性能不是问题。
  • “更易于管理”我指的是 XML 格式的人类可读性以及数据表示与类结构的解耦(您可以控制序列化 XML 的外观通过属性和结构不需要镜像实际的类)
【解决方案2】:

我们的应用程序在存储用户个人资料数据(网格列排列、过滤器设置...)时遇到了同样的问题。

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

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

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

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

  • 通过普通 BinaryFormatter 反序列化
  • 获取所有需要反序列化的字段(用自己的属性注释)
  • 用反序列化对象中的数据填充对象

适用于我们所有的数据,自三四个版本以来。

【讨论】:

    【解决方案3】:

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

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

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

    如果您更喜欢非二进制格式,JSON 通常是一个合适的选择。对于 RPC 和此类场景,Protobuf 更好,现在甚至被微软正式提及/认可:Introduction to gRPC on ASP.NET Core。 (gRPC 是建立在 Protobuf 之上的技术栈)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-08-28
      • 1970-01-01
      • 1970-01-01
      • 2011-02-18
      • 2020-10-16
      • 1970-01-01
      • 2019-02-16
      • 2018-03-17
      相关资源
      最近更新 更多