我想收集尽可能多的关于。net / clr中API版本控制的信息,特别是API更改如何破坏或不破坏客户端应用程序。首先,让我们定义一些术语:

API change - a change in the publicly visible definition of a type, including any of its public members. This includes changing type and member names, changing base type of a type, adding/removing interfaces from list of implemented interfaces of a type, adding/removing members (including overloads), changing member visibility, renaming method and type parameters, adding default values for method parameters, adding/removing attributes on types and members, and adding/removing generic type parameters on types and members (did I miss anything?). This does not include any changes in member bodies, or any changes to private members (i.e. we do not take into account Reflection).

二进制级中断——一种API更改,导致针对旧版本API编译的客户端程序集可能无法装入新版本。例如:改变方法签名,即使它允许以与以前相同的方式被调用(即:void返回类型/参数默认值重载)。

源代码级中断——API更改会导致针对旧版本API编写的现有代码可能无法使用新版本进行编译。但是,已经编译的客户机程序集与以前一样工作。例如:添加一个新的重载,可能导致之前明确的方法调用出现歧义。

源代码级安静的语义变化——API的变化会导致编写的现有代码针对旧版本的API悄悄地改变其语义,例如通过调用不同的方法。然而,代码应该继续编译,没有警告/错误,以前编译的程序集应该像以前一样工作。示例:在现有类上实现一个新接口,这会导致在重载解析过程中选择不同的重载。

The ultimate goal is to catalogize as many breaking and quiet semantics API changes as possible, and describe exact effect of breakage, and which languages are and are not affected by it. To expand on the latter: while some changes affect all languages universally (e.g. adding a new member to an interface will break implementations of that interface in any language), some require very specific language semantics to enter into play to get a break. This most typically involves method overloading, and, in general, anything having to do with implicit type conversions. There doesn't seem to be any way to define the "least common denominator" here even for CLS-conformant languages (i.e. those conforming at least to rules of "CLS consumer" as defined in CLI spec) - though I'll appreciate if someone corrects me as being wrong here - so this will have to go language by language. Those of most interest are naturally the ones that come with .NET out of the box: C#, VB and F#; but others, such as IronPython, IronRuby, Delphi Prism etc are also relevant. The more of a corner case it is, the more interesting it will be - things like removing members are pretty self-evident, but subtle interactions between e.g. method overloading, optional/default parameters, lambda type inference, and conversion operators can be very surprising at times.

这里有几个例子:

添加新的方法重载

Kind:源级中断

受影响的语言: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]);

添加新的隐式转换运算符重载

Kind:源级中断。

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


当前回答

推广到扩展方法

Kind:源级中断

受影响的语言:c# v6及更高版本(可能是其他语言?)

更改前的API:

public static class Foo
{
    public static void Bar(string x);
}

更改后的API:

public static class Foo
{
    public void Bar(this string x);
}

样例客户端代码在更改前工作,更改后失效:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

更多信息:https://github.com/dotnet/csharplang/issues/665

其他回答

这可能是一个不太明显的“添加/删除接口成员”的特殊情况,我认为它应该根据我接下来要发布的另一个情况单独进入。所以:

将接口成员重构为基接口

Kind:源级和二进制级都中断

受影响的语言:c#, VB, c++ /CLI, f#(用于源代码中断;二进制语言自然会影响任何语言)

更改前的API:

interface IFoo
{
    void Bar();
    void Baz();
}

更改后的API:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

被源代码级别的更改破坏的示例客户端代码:

class Foo : IFoo
{
   void IFoo.Bar() { ... }
   void IFoo.Baz() { ... }
}

被二进制级别的更改破坏的示例客户端代码;

(new Foo()).Bar();

注:

对于源级中断,问题是c#, VB和c++ /CLI在接口成员实现的声明中都要求精确的接口名称;因此,如果成员被移动到基接口,代码将不再编译。

二进制中断是由于接口方法在生成的IL中完全限定为显式实现,并且接口名称也必须是准确的。

在可用的情况下,隐式实现(即c#和c++ /CLI,但不是VB)在源代码和二进制级别上都可以很好地工作。方法调用也不会中断。

添加具有默认值的参数。

中断类型:二进制级别的中断

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

这是因为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#中默认值必须是编译时常量)。

重命名接口

休息一下:源代码和二进制文件

受影响的语言:很可能全部,用c#测试。

更改前的API:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

API变更后:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

示例客户端代码,可以工作,但随后被破坏:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break

将显式接口实现转换为隐式接口实现。

打破:来源

受影响语言:所有

将显式接口实现重构为隐式接口实现在如何破坏API方面更为微妙。从表面上看,这似乎是相对安全的,然而,当与继承结合在一起时,它可能会导致问题。

更改前的API:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}

API变更后:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator() { yield return "Foo"; }
}

样例客户端代码在更改前工作,更改后被破坏:

class Bar : Foo, IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
    { yield return "Bar"; }
}

foreach( var x in new Bar() )
    Console.WriteLine(x);    // originally output "Bar", now outputs "Foo"

到常量的静态只读转换

类型:二进制级别的中断

受影响的语言:c#, VB和f#

更改前的API:

public static class Foo
{
    public static readonly string Bar = "Value";
}

更改后的API:

public static class Foo
{
    public const string Bar = "Value";
}

所有客户端都需要重新编译以针对新的更改,否则会抛出MissingFieldException异常。