.NET 库与向后兼容的艺术 – 第二部分





5.00/5 (2投票s)
这是关于 .NET 库与向后兼容的艺术系列的第二篇文章。
- 第一部分 – 简介和行为不兼容性
- 第二部分 – 本文
- 第三部分 – 二进制不兼容性
第一部分围绕一条简单但难以遵循的规则:“不要在库中进行会改变其行为的更改”。仅此而已,就是不要这样做!
第二部分和第三部分讨论的是您可能想要向客户保证的其他类型的向后兼容性。您不必这样做,许多产品也不会这样做,但您应该提前决定并相应地设定期望。
源文件不兼容性
下一类不兼容性是最直接的:当客户更新您的库时,他们的项目将无法再编译。
源文件不兼容性永远不会被忽视!
这显然很烦人,因为您的客户现在不得不匆忙更新他们的代码。另一方面,这比库中发生静默的行为更改要好得多:源文件不兼容性永远不会被忽视!我们已经在上一篇文章中讨论过,您甚至可以使用 Obsolete 属性来强制源文件不兼容性,以保护您的用户免受行为更改的影响。
[Obsolete("This class is obsolete, use SeverelyBuggedClassV2 instead.", error: true)]
public class SeverelyBuggedClass
{
}
名称冲突
如果您的目标是永远不破坏源文件兼容性,请注意,您将无法完全保证这一点。至少,您无法控制客户项目中已使用了哪些类型名称,并且在向库中添加新类时可能会发生冲突。
这也不是世界末日,因为由此产生的错误非常直接且易于修复。
Error CS0104 'Foo' is an ambiguous reference between 'Namespace1.Foo' and 'Namespace2.Foo'
不过,解决这个错误可能会非常繁琐和耗时。至少,请确保不要使用与 Microsoft 广泛使用的 .NET 类型冲突的名称(例如,不要将您的类命名为 Int32
或 String
)。
一个有趣的边缘情况是,当一个新的方法签名(名称和参数类型)与客户定义的扩展方法冲突时。这实际上是一种行为不兼容性,因为它不会导致编译错误,而是会默默地将您的客户代码切换为使用新方法而不是扩展方法!
常见的源文件不兼容性
大多数源文件不兼容性类型都相当明显
- 重命名或删除类型、属性或方法
- 从方法中删除
virtual
- 向类添加
final
- 将方法、属性或字段更改为
static
或非static
- 向没有构造函数的类添加带参数的构造函数
- 将
public
类型设为internal
- 将
public
或protected
成员设为private
或internal
- 向方法添加非可选参数
- 更改方法参数类型(除非存在隐式转换,例如,将
short
更改为long
是可以的) - 更改属性、字段和方法的返回类型(除非存在隐式转换,例如,将
long
更改为short
是可以的) - 更改方法参数修饰符(in、out、ref 或删除 params)
- 重命名方法参数(这会破坏 命名参数的使用)
- 向泛型类型添加 类型约束
- 等等。
回去再读一遍列表,我敢肯定有几项您忽略了。
(我知道在撰写本文时,我不得不回去并多次添加内容……)
照片由 Mollybob 拍摄,在知识共享许可下使用。
进行任何这些更改仅会影响 public
类型(或 public
非 final 类型的 protected
成员)的 public
成员:如果您的客户无法使用您更改的内容,那么您就不会破坏他们。对此有一个例外:反射。
Reflection(反射)
反射允许访问通常不可见的类型和成员。这违反了所有封装规则,除非您已指示您的客户在您的库上使用反射,否则这是一种非常糟糕的做法。
您可能希望在文档中明确说明,您库的所有私有部分都可以随时更改,恕不另行通知,也不提供任何向后兼容性保证。除此之外,我认为大多数在他人库上使用反射的客户都知道他们的代码可能会中断。
如果您在库中使用反射(确实有一些合理的使用场景),请确保您有针对该代码的单元测试,因为在进行更改时,您很容易破坏自己的库。
接口和抽象类
虽然大多数源文件不兼容性源于删除客户正在使用的功能,但添加约束也是一个问题。
这个问题最常见的原因是在 public
接口上添加方法或属性,或者向类添加 abstract
成员。这很容易被忽略为“添加功能”,但如果客户正在实现接口(或扩展 abstract
类),他们现在将不得不更改代码以实现新成员。
隐式类型转换
不幸的是,很少有工具可以解决引入源文件不兼容性的问题。在大多数情况下,这只是一个关于首先进行良好设计以及之后进行代码更改时要小心的问题。
我们有一些语言支持的领域是更改方法、属性和字段的输入和输出类型。我们可以定义 隐式转换运算符 以保持源文件兼容性。
例如,假设我们有这个方法
public class Calendar
{
public DateTime FindNextAppointment(DateTime start);
}
并且我们不喜欢 .NET 中的 DateTime
类型可以是 Utc
、Local
甚至 Unspecified
,这使得此方法容易出错。
如果我们提供隐式转换,我们可以更改其参数和返回类型为其他类型。
public class Calendar
{
public UtcDateTime FindNextAppointment(UtcDateTime start);
}
public struct UtcDateTime
{
private readonly DateTime Time;
public UtcDateTime(DateTime time)
{
switch (time.Kind)
{
case DateTimeKind.Utc:
Time = time;
break;
case DateTimeKind.Local:
Time = time.ToUniversalTime();
break;
default:
throw new NotSupportedException
("UtcDateTime cannot be initialized with an Unspecified DateTime.");
}
}
public static implicit operator UtcDateTime(DateTime t) => new UtcDateTime(t);
public static implicit operator DateTime(UtcDateTime t) => t.Time;
}
此更改是源文件兼容的(但不是行为兼容的,因为我们现在抛出 NotSupportedException
)。
What Next?
下一篇博文将介绍向客户保证二进制兼容性的优点和缺点,以及如何为您的库实际维护二进制兼容性。