在.NET中打破变化

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

添加新的实例方法

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扩展方法作为静态方法进行调用。


更改方法签名

种类:二进制级别的突破

受影响的语言:C#(VB和F#最有可能,但未经测试)

更改前的API

public static class Foo
{
    public static void bar(int i);
}

更改后的API

public static class Foo
{
    public static bool bar(int i);
}

示例客户端代码在更改前工作

Foo.bar(13);

添加一个默认值的参数。

一种突破:二进制级别的突破

即使调用源代码不需要更改,仍然需要重新编译(就像添加常规参数一样)。

这是因为C#将参数的默认值直接编译到调用程序集中。 这意味着如果你不重新编译,你会得到一个MissingMethodException,因为旧程序集试图调用一个参数较少的方法。

更改前的API

public void Foo(int a) { }

更改后的API

public void Foo(int a, string b = null) { }

示例客户端代码之后被破解

Foo(5);

客户端代码需要在字节码级别重新编译为Foo(5, null) 。 被调用的程序集将只包含Foo(int, string) ,而不是Foo(int) 。 这是因为默认参数值纯粹是一种语言功能,.Net运行时不知道关于它们的任何信息。 (这也解释了为什么默认值必须是C#中的编译时常量)。


当我发现它时,这一点非常不明显,特别是鉴于接口的相同情况的不同。 这根本不是一个休息时间,但是我决定将其包含在内,这一点令人惊讶:

将类成员重构成基类

善良:不是休息!

受影响的语言:无(即没有损坏)

更改前的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++/CLI
ref 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是一个阶级是什么使差异。 去搞清楚...

链接地址: http://www.djcxy.com/p/86111.html

上一篇: breaking changes in .NET

下一篇: Automatically update version number