我想尽可能多地收集有关 .NET/CLR 中 API 版本控制的信息,特别是 API 更改如何破坏或不破坏客户端应用程序。首先,让我们定义一些术语:
API 更改- 公开可见的类型定义的更改,包括其任何公共成员。这包括更改类型和成员名称、更改类型的基类型、从类型的已实现接口列表中添加/删除接口、添加/删除成员(包括重载)、更改成员可见性、重命名方法和类型参数、添加默认值对于方法参数,添加/删除类型和成员的属性,以及添加/删除类型和成员的泛型类型参数(我错过了什么吗?)。这不包括成员机构的任何变化,或私人成员的任何变化(即我们不考虑反射)。
二进制级中断- API 更改导致针对旧版本 API 编译的客户端程序集可能不会与新版本一起加载。示例:更改方法签名,即使它允许以与以前相同的方式调用(即:void 返回类型/参数默认值重载)。
源代码级中断- 一种 API 更改,导致编写的现有代码针对旧版本的 API 进行编译可能无法与新版本一起编译。然而,已经编译的客户端程序集像以前一样工作。示例:添加一个新的重载可能会导致之前明确的方法调用出现歧义。
源代码级别的安静语义更改- 一种 API 更改,导致编写用于针对旧版本 API 进行编译的现有代码悄悄地更改其语义,例如通过调用不同的方法。然而,代码应该继续编译而没有警告/错误,并且以前编译的程序集应该像以前一样工作。示例:在现有类上实现一个新接口,导致在重载决议期间选择不同的重载。
最终目标是对尽可能多的中断和安静语义 API 更改进行分类,并描述中断的确切影响,以及哪些语言受其影响和不受其影响。扩展后者:虽然某些更改普遍影响所有语言(例如,向接口添加新成员将破坏该接口在任何语言中的实现),但有些更改需要非常特定的语言语义才能发挥作用才能获得突破。这通常涉及方法重载,并且通常涉及与隐式类型转换有关的任何事情。即使对于符合 CLS 的语言(即至少符合 CLI 规范中定义的“CLS 消费者”规则的语言),似乎也没有任何方法可以在这里定义“最小公分母”——尽管我 如果有人在这里纠正我的错误,我将不胜感激 - 所以这将不得不按语言进行。最感兴趣的自然是 .NET 开箱即用的那些:C#、VB 和 F#;但其他的,如 IronPython、IronRuby、Delphi Prism 等也是相关的。越是极端情况,它就越有趣——移除成员之类的事情是不言而喻的,但是方法重载、可选/默认参数、lambda 类型推断和转换运算符之间的微妙交互可能会非常令人惊讶有时。
几个例子来启动这个:
添加新的方法重载
种类:源级中断
受影响的语言:C#、VB、F#
变更前的 API:
public class Foo
{
public void Bar(IEnumerable x);
}
变更后的API:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
示例客户端代码在更改之前工作并在更改之后中断:
new Foo().Bar(new int[0]);
添加新的隐式转换运算符重载
种类:源级中断。
受影响的语言:C#、VB
不受影响的语言:F#
变更前的 API:
public class Foo
{
public static implicit operator int ();
}
变更后的API:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
示例客户端代码在更改之前工作并在更改之后中断:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
注意:F# 没有损坏,因为它没有任何语言级别的对重载运算符的支持,无论是显式的还是隐式的 - 都必须直接调用op_Explicit
和op_Implicit
方法。
添加新的实例方法
Kind:源级安静的语义变化。
受影响的语言:C#、VB
不受影响的语言:F#
变更前的 API:
public class Foo
{
}
变更后的API:
public class Foo
{
public void Bar();
}
遭受安静语义更改的示例客户端代码:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
注意:F# 没有损坏,因为它没有语言级别的支持ExtensionMethodAttribute
,并且需要将 CLS 扩展方法作为静态方法调用。