目录
源代码不兼容
名称冲突
常见的源代码不兼容
反射
接口和抽象类
隐式类型转换
接下来是什么?
这是.NET库和向后兼容系列技术的第二篇文章:
- 第1部分——简介和行为不兼容
- 第2部分——这篇文章
- 第3部分——二进制不兼容
第1部分是关于一个简单但难以遵循的规则:“不要对您的库进行更改以改变其行为”。就是这样,只是不要这样做!
第2部分和第3部分是您可能希望向客户保证的其他类型的向后兼容性。您不必这样做,很多产品都不需要,但是您应该事先决定并相应地设定期望。
源代码不兼容下一种不兼容性是最直接的:当客户更新您的库时,他们的项目不再编译。
源代码不兼容永远不会被忽视!
这显然很烦人,因为您的客户现在必须争先恐后地更新他们的代码。另一方面,这比您的库中进行无提示的行为更改要好得多:源代码不兼容永远不会被忽视!我们已经在上一篇文章中讨论了如何甚至使用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添加到类上
- 将方法、属性或字段更改为静态或非静态
- 将具有参数的构造函数添加到没有构造函数的类中
- 将public设为internal
- 将public或protected的成员设为private或internal
- 向方法添加非可选参数
- 更改方法参数类型(除非隐式转换可用,例如可以将short更改为long)
- 更改属性、字段和方法的返回类型(除非隐式转换可用,例如long到short都是可以的)
- 更改方法参数修饰符(in,out,ref或删除params)
- 重命名方法参数(这会破坏命名参数的使用)
- 在通用类型上添加类型约束
- 等等
返回并再次阅读列表,我确定您忽略了几项。
(我知道写这篇文章时必须多次返回并添加到列表中……)
进行任何更改仅是公共类型的公共成员(或公共非最终类型的受保护成员)的问题:如果您的客户无法使用所做的更改,则不会破坏它们。该规则有一个例外:Reflection。
反射反射允许访问通常不可见的类型和成员。这违反了所有封装规则,除非您已指示客户在库中使用反射,否则这是非常不好的做法。
您可能希望在文档中明确说明,并声明可以更改库的所有私有部分,恕不另行通知,并且不提供任何向后兼容性保证。除此之外,我认为大多数在其他人的库上使用反射的客户都知道他们的代码可能会中断。
如果您在库中使用反射(确实有一些合理的用例),请确保对该代码进行单元测试,因为在进行更改时很容易破坏自己的库。
接口和抽象类尽管大多源代码不兼容性来自客户使用的功能的删除,但添加约束也是一个问题。
此问题的最常见来源是在公共接口上添加方法或属性,或者在类中添加抽象成员。这很容易被忽略为“添加功能”,但是,如果客户正在实现接口(或扩展抽象类),那么他们现在将不得不更改其代码以实现新成员。
隐式类型转换不幸的是,很少有工具可以解决源代码不兼容的问题。在大多数情况下,这只是要做一个好的设计,然后在以后进行代码更改时要小心。
我们提供某种语言支持的一个领域是更改方法、属性和字段的输入和输出类型。我们可以定义隐式转换运算符以保持更改源兼容。
例如,假设我们有这种方法
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)。
接下来是什么?下一篇博客文章将介绍保证与客户的二进制兼容性以及如何实际维护库的二进制兼容性的利弊。