猿问
下载APP

.NET中API破坏性更改的权威指南

.NET中API破坏性更改的权威指南

我想尽可能多地收集有关.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_Explicitop_Implicit方法。

添加新的实例方法

种类:源级静默语义变化。

受影响的语言: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扩展方法作为静态方法调用。


郎朗坤
浏览 50回答 3
3回答

qq_笑_17

更改方法签名种类:二级休息受影响的语言:C#(VB和F#最有可能,但未经测试)更改前的APIpublic static class Foo{     public static void bar(int i);}更改后的APIpublic static class Foo{     public static bool bar(int i);}变更之前的客户端代码示例Foo.bar(13);

智慧大石

添加具有默认值的参数。休息时间:二元级休息即使调用源代码不需要更改,仍然需要重新编译(就像添加常规参数一样)。这是因为C#直接将参数的默认值编译到调用程序集中。这意味着如果你不重新编译,你将得到一个MissingMethodException,因为旧的程序集试图调用一个参数较少的方法。更改前的APIpublic void Foo(int a) { }变更后的APIpublic void Foo(int a, string b = null) { }之后破坏的示例客户端代码Foo(5);客户端代码需要Foo(5, null)在字节码级别重新编译。被调用的程序集只包含Foo(int, string),而不是Foo(int)。这是因为默认参数值纯粹是一种语言特性,.Net运行时对它们一无所知。(这也解释了为什么默认值必须是C#中的编译时常量)。

慕桂英546537

当我发现它时,这个非常不明显,特别是考虑到界面相同情况的不同。这根本不是休息,但令人惊讶的是我决定将它包括在内:将类成员重构为基类善良:不休息!受影响的语言:无(即没有破坏)更改前的API:class Foo{     public virtual void Bar() {}     public virtual void Baz() {}}更改后的API:class FooBase{     public virtual void Bar() {}}class Foo : FooBase{     public virtual void Baz() {}}在整个更改过程中保持工作的示例代码(即使我预计它会中断):// C++/CLIref class Derived : Foo{    public virtual void Baz() {{    // Explicit override        public virtual void BarOverride() = Foo::Bar {}};笔记:C ++ / CLI是唯一具有类似于虚拟基类成员的显式接口实现的构造的.NET语言 - “显式覆盖”。我完全期望导致与将接口成员移动到基接口时相同的破坏(因为为显式覆盖生成的IL与显式实现相同)。令我惊讶的是,事实并非如此 - 即使生成的IL仍然指定BarOverride覆盖Foo::Bar而不是FooBase::Bar,汇编加载器足够智能,可以正确地替换另一个而没有任何抱怨 - 显然,这Foo是一个类的事实是产生差异的原因。去搞清楚...
打开App,查看更多内容
随时随地看视频慕课网APP
我要回答