在整个历史记录中,.NET 尝试保持从版本到版本以及跨 .NET 实现的高兼容性。 尽管与 .NET Framework 相比,.NET 5(和 .NET Core)和更高版本可以被视为新技术,但两个主要因素限制了 .NET 实现与 .NET Framework 的分离能力:
许多开发人员最初开发或继续开发 .NET Framework 应用程序。 他们期望在 .NET 实现中保持一致的行为。
.NET Standard 库项目允许开发人员创建面向 .NET Framework 和 .NET 5(以及 .NET Core)及更高版本共享的常见 API 的库。 开发人员期望 .NET 5 应用程序中使用的库的行为应与 .NET Framework 应用程序中使用的库相同。
除了跨 .NET 实现的兼容性之外,开发人员还期望在给定的 .NET 实现的版本之间实现高级别的兼容性。 具体而言,为早期版本的 .NET Core 编写的代码应在 .NET 5 或更高版本上无缝运行。 事实上,许多开发人员希望新发布的 .NET 版本中找到的新 API 也应与引入这些 API 的预发行版本兼容。
本文概述了影响兼容性的更改,以及 .NET 团队评估每种类型的更改的方式。 如果开发人员需打开要求修改 现有 .NET API 的行为的拉取请求,则了解 .NET 团队如何处理可能的中断性变更对他们来说尤其有用。
以下部分介绍对 .NET API 所做的更改类别及其对应用程序兼容性的影响。 更改要么是允许的(✔️),不允许(❌),或者需要判断和评估前一行为是多么可预测、明显和一致(❓)。
注释
- 除了作为如何评估 .NET 库更改的指南之外,库开发人员还可以使用这些条件来评估针对多个 .NET 实现和版本的库的更改。
- 有关兼容性类别(例如向前和向后兼容性)的信息,请参阅 代码更改如何影响兼容性。
对公共协定的修改
此类别的变更会修改类型的公共外围应用。 此类别中的大多数更改都是不允许的,因为它们违反了 向后兼容性 (使用以前版本的 API 开发的应用程序的能力),无需在更高版本上重新编译即可执行这些更改。
类型
✔️ ALLOWED:当接口已由基类型实现时,从类型中删除接口实现
❓ 需要判断:向类型添加新接口实现
这是可接受的更改,因为它不会对现有客户端产生不利影响。 对类型所做的任何更改都必须在此处定义的可接受更改的边界内工作,以便新实现保持可接受。 添加直接影响设计器或序列化程序功能(生成无法供低级使用的代码或数据的功能)的接口时,需要格外注意。 例如接口 ISerializable 。
❓ 需要判断:引入新的基类
如果类型不引入任何新的 抽象 成员或更改现有类型的语义或行为,则可以在两个现有类型之间引入层次结构。 例如,在 .NET Framework 2.0 中,DbConnection 类成为 SqlConnection 的新基类,SqlConnection 之前是直接从 派生的。
✔️ 允许:将类型从一个程序集移到另一个程序集
旧程序集必须标记为指向新程序集的。
✔️ 允许:将结构体类型更改为
readonly struct
类型不允许将
readonly struct
类型更改为类型struct
。✔️ 允许:扩展类型的可见性
❌ 不允许:更改类型的命名空间或名称
❌ 不允许:重命名或删除公共类型
这将中断使用重命名的或删除的类型的所有代码。
注释
在极少数情况下,.NET 可能会删除公共 API。 有关详细信息,请参阅 .NET 中的 API 删除。 有关 .NET 的支持策略,请参阅 .NET 支持策略。
❌ 不允许:更改枚举的基础类型
这是一次编译时和运行时破坏性变化,同时也是二进制破坏性变化,可能导致属性参数无法解析。
❌ 不允许:密封之前未密封的类型
❌ 不允许:向接口的基类型集添加接口
如果接口实现以前未实现的接口,则实现接口的原始版本的所有类型都会中断。
❓ 需要评判:从一组基类删除某个类,或从一组实现的接口删除某个接口
删除接口的规则有一个例外:可以添加派生自已删除接口的接口的实现。 例如,如果类型或接口现在实现了IDisposable,而IDisposable实现了IDisposable,那么可以删除。
❌ 不允许:将
readonly struct
类型更改为 struct 类型但允许将
struct
类型更改为readonly struct
类型。❌ 不允许:将 结构 类型更改为类型
ref struct
,反之亦然❌ 不允许:缩小类型的可见性
但允许增大类型的可见性。
成员
✔️ 允许:扩展非虚拟成员的可见性
✔️ 允许:将抽象成员添加到没有 可访问(公共或受保护)构造函数的公共类型中,或者该类型是密封的
但是,不允许将抽象成员添加到具有可访问的(public或protected)构造函数且不是
sealed
的类型。✔️ 允许:将成员移动到层次结构中高于删除的成员所在的类型的类
✔️ 允许:添加或删除重写
引入重载可能导致先前的用户在调用 base 时跳过重载。
✔️ ALLOWED:将构造函数添加到类,如果类以前没有构造函数,则添加无参数构造函数
但是,不允许在未添加无参数构造函数 的情况下 将构造函数添加到以前没有构造函数的类。
✔️ 允许:从
ref readonly
更改为ref
返回值(虚拟方法或接口除外)✔️ 允许:若字段的静态类型为非可变的值类型,从字段删除 readonly
✔️ 允许:调用之前未定义的新事件
❓ 需要斟酌:向类型添加新实例字段
此更改会影响序列化。
❌ 不允许:重命名或删除公共成员或参数
这会中断使用重命名或删除的成员或参数的所有代码。
这包括删除或重命名属性中的 Getter 或资源库,以及重命名或删除枚举成员。
❌ 不允许:向接口添加成员
如果你提供一个实现,则向现有接口添加新成员不一定会导致下游程序集中出现编译失败。 但是,并非所有语言都支持默认接口成员(DIM)。 此外,在某些情况下,运行时无法决定要调用的默认接口成员。 出于这些原因,将成员添加到现有接口被视为破坏性变更。
❌ 不允许:更改公共常量或枚举成员的值
❌ 禁止:更改属性、字段、参数或返回值的类型
❌ 不允许:添加、删除或更改参数顺序
❌ 不允许:重命名参数(包括更改其大小写)
鉴于以下两个原因将此视为中断性变更:
❌ 不允许:从
ref
返回值更改为ref readonly
返回值❌️ 不允许:在虚拟方法或接口上从
ref readonly
更改为ref
返回值❌ 不允许:向成员添加或从中删除 abstract
❌ 不允许:从成员删除 virtual 关键字
❌ 不允许:向成员添加 virtual 关键字
虽然这通常不是中断性变更,因为 C# 编译器倾向于发出 调用virt 中间语言(IL)指令来调用非虚拟方法(
callvirt
执行 null 检查,而正常调用不执行),但出于多种原因,此行为并非不变:使方法虚拟意味着使用者代码最终会以非虚拟方式调用它。
❌ 不允许:使虚拟成员成为抽象成员
❌ 不允许:将 sealed 关键字添加到接口成员
将
sealed
添加到默认接口成员中将使该成员成为非虚拟的,进而阻止派生类型对该成员的实现被调用。❌ 不允许:向包含可访问的(public 或 protected)构造函数且非 sealed 类型的公共类型添加抽象成员
❌ 不允许:向成员添加或从中删除 static 关键字
❌ 不允许:添加排除现有重载并定义其他行为的重载
这将中断已绑定先前重载的现有客户端。 例如,若类包含单个接受 UInt32 的方法的版本,传递 Int32 值时,现有使用者将成功地绑定该重载。 但是,如果添加接受 Int32 的重载,重新编译或使用晚期绑定时,编译器现在将绑定新的重载。 若生成不同的行为,则它属于中断性变更。
❌ 不允许:只向过去不包含任何构造函数的类添加构造函数而不添加无参数构造函数
❌️ 不允许:向字段添加 readonly
❌ 不允许:缩小成员的可见性
这包括在存在 可访问 的构造函数( 或
public
)且类型protected
密封的情况下,减少 受保护 成员的可见性。 如果不是这种情况,则允许减少受保护成员的可见性。允许增加成员的可见性。
❌ 不允许:更改成员的类型
无法修改方法的返回值或属性或字段的类型。 例如,返回 Object 方法的签名不能更改以返回 a String,反之亦然。
❌ 不允许:将实例字段添加到没有非公共字段的结构
如果结构只有公共字段或根本没有字段,调用方可以声明该结构类型的局部变量,而无需调用结构的构造函数或首先初始化本地
default(T)
,只要在首次使用之前将所有公共字段设置完毕。 将任何新字段(公共或非公共字段)添加到此类结构对这些调用者来说是一个源代码破坏性变更,因为编译器现在需要初始化这些额外的字段。此外,对于已将
[SkipLocalsInit]
应用于其代码的调用方来说,将任何新字段(公共或非公共)添加到没有字段或只有公共字段的结构都是二进制中断性变更。 由于编译器在编译时并未意识到这些字段,它可能会生成未完全初始化结构的中间语言(IL),这会导致结构体被创建于未初始化的堆栈数据。如果结构具有任何非公共字段,编译器已通过构造函数
default(T)
强制初始化,并且添加新实例字段不是中断性变更。❌ 不允许:触发先前从未触发过的现有事件
行为变更
程序集
✔️ 允许:在仍支持同一平台的情况下,使程序集具备可移植性
❌ 不允许:更改程序集的名称
❌ 不允许:更改程序集的公钥
属性、字段、参数和返回值
✔️ ALLOWED:将属性、字段、返回值或 out 参数的值更改为更派生的类型
✔️ ALLOWED:如果成员不是虚拟的,则增加属性或参数接受的值的范围
虽然可以传递给方法或成员返回的值范围可以展开,但参数或成员类型不能。 例如,虽然传递给方法的值可以从 0-124 扩展到 0-255,但参数类型不能从 Byte 更改为 Int32。
❌ 不允许:在成员为 virtual 成员时,扩大属性或参数的可接受值的范围
此变更将中断已重写的现有成员,面向扩展的值范围时它们将无法正常运行。
❌ 不允许:减少属性或参数的接受值范围
❌ 不允许:增加属性、字段、返回值或 out 参数的返回值范围
❌ 不允许:更改属性、字段、方法的返回值或 out 参数
❌ DISALLOWED:更改属性、字段或参数的默认值
更改或删除 参数 默认值不会导致二进制兼容性问题。 删除参数默认值是源中断,更改参数默认值可能会导致重新编译后发生行为中断。
因此,在将这些默认值“移动到新方法重载”以消除歧义的特定情况下,删除参数默认值是可以接受的。 例如,请考虑现有的方法
MyMethod(int a = 1)
。 如果引入了具有两个可选参数MyMethod
a
的b
重载,则可以通过将默认值a
移动到新重载来保留兼容性。 现在,这两个重载为MyMethod(int a)
和MyMethod(int a = 1, int b = 2)
。 此模式允许MyMethod()
编译。❌ 不允许:更改数值返回值的精度
❓ 需要评判:关于输入分析和新异常引发的变更(尽管本文档未指定分析行为)
例外
✔️ 允许:引发派生程度高于现有异常的异常
由于新异常是现有异常的子类,因此以前的异常处理代码将继续处理异常。 例如,在 .NET Framework 4 中,找不到区域性时,区域性生成和检索方法开始引发 CultureNotFoundException 而不引发 ArgumentException。 由于 CultureNotFoundException 派生自 ArgumentException,因此这是可接受的更改。
✔️ 允许:引发比NotSupportedException、NotImplementedException、NullReferenceException更具体的异常
✔️ 允许:引发被视为不可恢复的异常
不应捕获无法恢复的异常,而应该由高级别的全部捕获处理程序处理它们。 因此,用户不应有捕获这些显式异常的代码。 不可恢复的异常包括:
✔️ 允许:在新的代码路径中引发新的异常
该异常必须仅适用于使用新参数值或状态执行的新代码路径,并且不能由面向以前版本的现有代码执行。
✔️ 允许:删除异常以启用更可靠的行为或新方案
例如,
Divide
方法以前只处理正值并在其他情况下抛出 ArgumentOutOfRangeException 异常,现在可以更改为同时支持负值和正值,而不会抛出异常。✔️ 允许:更改错误消息的文本
开发人员不应依赖错误消息的文本,而错误消息也会根据用户的区域性而更改。
❌ 不允许:在上文未列出的任何其他的情况下引发异常
❌ 不允许:在上文未列出的任何其他的情况下删除异常
特性
✔️ 允许:更改不可观测的属性的值
❌ 不允许:更改 可观测的属性 的值
❓ 需要判断:删除属性
在大多数情况下,删除属性(例如 NonSerializedAttribute)是一项重大更改。
平台支持
✔️ 允许:支持在以前不支持的平台上进行的操作
❌ 不允许:对于平台先前支持的操作,不再支持或者现在需要特定服务包
内部实现更改
❓ 需要评判:更改内部类型的外围应用
尽管此类更改将中断私有反射,但通常允许这些变更。 在某些情况下,如果常用的第三方库或大量开发人员依赖于内部 API,则不允许进行此类更改。
❓ 需要评判:更改成员的内部实现
尽管此类更改将中断私有反射,但通常允许这些变更。 在某些情况下,客户代码经常依赖于专用反射或更改引入意外的副作用,则不允许进行这些更改。
✔️ 允许:提高操作的性能
修改操作性能的能力至关重要,但此类更改可能会破坏依赖于操作当前速度的代码。 这尤其适用于依赖于异步作计时的代码。 性能调整不应影响相关API的其他行为;否则,这种更改就会导致破坏性修改。
✔️ 允许:间接(且经常是不利地)更改操作性能
如果因其他原因未将所做更改分类为中断性变更,则这是可以接受的。 通常,需要采取行动,这可能包括额外操作或添加新功能。 这几乎总是影响性能,但可能需要使有问题的 API 按预期运行。
❌ 不允许:将同步 API 更改为异步 API(反之亦然)
代码更改
✔️ 允许:向参数添加参数
❌ 不允许:向代码块添加 checked 语句
此变更可能导致先前执行的代码引发 OverflowException,此为不可接受的变更。
❌ 不允许:从参数删除 params
❌ 不允许:更改事件的触发顺序
开发人员可以合理地期望事件按相同的顺序触发,开发人员代码通常取决于触发事件的顺序。
❌ 不允许:删除给定操作上的事件引发
❌ 不允许:更改调用给定事件的次数
❌ 不允许将 FlagsAttribute 添加到枚举类型中