一个明确的指导API断变化。净
-
12-09-2019 - |
题
我要收集尽可能多的信息,关于API版本。NET/CLR,具体如何API改变或不断客户的应用。第一,让我们来定义的一些条款:
API改变 -改变公开可见定义的类型,包括其任何公众成员。这包括更改类型和成员的姓名、改变基类型的类型,加入/消除接口从名单实现接口的类型,加入/消除成员(包括重载),改变部件的能见度,重新命名方法和类型参数,加上默认值的方法的参数,增加或删除的属性在类型和成员,并加入/消除的一般类型参数上的类型和成员(难道我错过了什么吗?).这不包括任何变化在成员机构,或者任何变化,私人成员(即我们不考虑到反射)。
二级破 -API改变,结果在客户组件编制针对老年版的API可能不装载的新版本。例如:改变方法的签名,即使它允许被称为以同样的方式为之前(即:void返回型参数的缺省值超载).
源级打破 -API改变,结果在现有的编写的代码汇编对老年版的API潜在的不汇编的新版本。已经编制客户组的工作之前,但是。例如:增加一个新的过负荷,可以导致含糊不清的方法的呼吁,毫不含糊的前面。
源水平宁静的语义的改变 -API改变,结果在现有的编写的代码汇编对老年版的API悄然改变它的语义,例如通过调用不同的方法。代码但是应该继续汇编没有警告/错误,并预先编译程序集的工作不如以前。例如:实施一个新的界面上现有类,结果在不同的过载是选择在过载决议。
最终的目标是catalogize多的破坏和安静的语义API变化成为可能,并且描述了精确的效果的破损,以及哪种语言并未受到它的影响。扩大后者:虽然有些变化影响的所有语言的普遍(例如增加一个新成员一个接口,将破坏实现该接口在任何语言),一些需要非常具体的语言的语义进入玩得到一个突破。这个最典型地涉及方法的重载,并在一般情况下,任何具有与隐含的类型转换。似乎没有以任何方式来定义的"最小公分母",这里甚至CLS符合语言(即那些符合至少要规则"CLS消费"的定义在CLI spec)-虽然,我将不胜感激,如果有人纠正我的错误在这里-因此,这将不得不去语言的语言。那些最感兴趣的自然是那些到来。净出的盒子:C#、VB和F#;但其他人,例如IronPython,如何从语言特性,德尔福棱镜等也是相关的。更多的一个角落的情况下,更有趣它将如去除成员都是不言自明的,但微妙的相互作用之间如方法,重载,可缺省参数,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
方法。
增加新的实例的方法
类型:源水平宁静的语义的变化。
语言的影响: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是唯一的。净的语言,有一个结构类似于明确的界面实现虚拟基类成员的"明确复盖".我完全预计到的结果在同样的破裂时移动接口成员的一个基本界面(由于IL产生对于明确的复盖相同的明确的实现)。我惊讶的是,这个不是这种情况下,即使产生的IL仍然规定, BarOverride
复盖 Foo::Bar
而不是 FooBase::Bar
, ,组件装载程序是足够的智慧来替代一个用另一个正确的,没有任何投诉显然,事实上, Foo
是一类的,是什么使差异。去图...
这是一个“添加/删除接口成员”的也许不是那么明显的特殊情况,我想它值得在我接下来会发布另一种情况的光其自身的条目。所以:
重构接口部件为基本接口
种:在源代码和二进制水平断裂
语言影响: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)将工作源代码和二进制级上的罚款。方法调用不破无论是。
重新排序枚举值
类断裂:的源代码级/二进制级别的安静语义更改
影响语言:所有
重新排序枚举值将保持源级兼容性作为文字具有相同的名称,但他们的序索引将被更新,这可能会导致一些种无声源级断裂。
更糟糕的是,如果客户端代码没有对新的API版本编译可以引入沉默二进制级别的休息时间。枚举值是编译时间常数和作为其中的任何这样的用途烘烤成客户端组件的IL。这种情况下可以是特别硬有时被发现。
API更改
之前public enum Foo
{
Bar,
Baz
}
API更改
后public enum Foo
{
Baz,
Bar
}
示例的客户端代码,工程,但事后破:
Foo.Bar < Foo.Baz
这是一个真正在实践中非常罕见的事情,但仍然令人惊讶的一个,当它发生。
添加新的非过负荷成员
类:源水平断裂或安静语义变化
语言影响:C#,VB
语言不受影响:F#,C ++ / CLI
改变之前API:
public class Foo
{
}
改变之后API:
public class Foo
{
public void Frob() {}
}
示例客户机代码由改变破:
class Bar
{
public void Frob() {}
}
class Program
{
static void Qux(Action<Foo> a)
{
}
static void Qux(Action<Bar> a)
{
}
static void Main()
{
Qux(x => x.Frob());
}
}
注意:
这里的问题是由在C#和VB在重载解析的存在λ型推理引起的。在这里采用duck类型的有限形式打破其中多于一种类型的匹配关系,通过检查拉姆达的主体是否使一个给定类型的感 - 。如果只有一种类型的结果在可编译体,即一个选择
在这里的危险是,客户端代码可能有一个重载的方法组,其中一些方法拿自己的类型的参数,以及其他利用您的图书馆暴露的类型参数。如果他的任何代码,然后依靠类型推断算法来确定仅基于成员的存在或不存在正确的方法,然后添加一个新成员,以你的类型具有相同的名称之一,因为在客户端的类型有可能抛出的推论之一断,从而导致模糊过载解析期间。
请注意,在该示例性类型Foo
和Bar
不以任何方式相关的,而不是通过继承也非其它方面。在一个单一的方法组仅仅使用它们中的是足以触发此,如果这发生在客户端代码,必须在它的控制。
上面的示例代码演示了一个更简单的情况,其中,这是一个源级中断(即编译器错误的结果)。然而,这也可以是一个无声的语义改变,如果这是通过推理选择的过载有其他的参数,否则将导致它的下方(例如可选参数之间进行排名以缺省值,或类型不匹配声明和实际参数,需要一个隐式转换)。在这样的情况下,重载解析将不再失败,但不同的超载会被编译器悄然选择。然而在实践中,这是非常难以没有仔细构建方法签名来故意使其碰上这种情况下。
转换的隐式接口实现为一个显式的一个。
歇的种类:源代码和二进制
语言受影响的:所有的
这是真的只是一个改变的方法的可访问性的变化 - 它只是一个小更微妙的,因为它很容易忽略一个事实,即并非所有访问接口的方法是一定要通过该接口的类型的引用
API改变之前:
public class Foo : IEnumerable
{
public IEnumerator GetEnumerator();
}
API更改后:
public class Foo : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator();
}
改变之前的作品,并且事后破示例客户端代码:
new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
转换的显式接口实现为一个隐含的一个。
歇的种类:来源
语言受影响的:所有的
显式接口实现的重构成隐式之一是在其如何可以打破的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"
更改的字段的属性
歇的种类:API
语言受影响:Visual Basic和C#*
的信息:当更改正常字段或变量成在视觉基本属性,任何外来代码引用该构件以任何方式将需要重新编译。
<强> API改变之前:强>
Public Class Foo
Public Shared Bar As String = ""
End Class
<强> API更改后:强>
Public Class Foo
Private Shared _Bar As String = ""
Public Shared Property Bar As String
Get
Return _Bar
End Get
Set(value As String)
_Bar = value
End Set
End Property
End Class
<强>样品的客户端代码,工程,但事后破:强>
Foo.Bar = "foobar"
命名空间加成
<强>源代码级的中断/源代码级的安静语义更改
由于道路命名空间解析工作在vb.Net,添加命名空间到库可能会导致与以前版本的API编写不与新版本编译的Visual Basic代码。
示例客户机代码:
Imports System
Imports Api.SomeNamespace
Public Class Foo
Public Sub Bar()
Dim dr As Data.DataRow
End Sub
End Class
如果该API的新版本增加的命名空间Api.SomeNamespace.Data
,则以上代码不能编译。
它成为与项目级别的命名空间的进口更复杂。如果Imports System
从上述代码省略,但System
命名空间在项目级导入,则代码仍可能导致错误。
然而,如果API包括在其DataRow
命名空间的类Api.SomeNamespace.Data
,则代码将编译,但是当与API的新版本在编译时与旧版本的API和dr
的编译System.Data.DataRow
将Api.SomeNamespace.Data.DataRow
的一个实例
参数重命名
<强>源代码级的断裂强>
更改的参数的名字是从第7版(?)(.NET版本1?)和C#.NET从版本4(.NET版本4)。
在vb.net重大更改改变之前API:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
改变之后API:
namespace SomeNamespace {
public class Foo {
public static void Bar(string y) {
...
}
}
}
示例客户机代码:
Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB
ref参数
<强>源代码级的断裂强>
添加具有相同签名的方法覆盖除了一个参数通过引用而不是通过值传递将导致引用该API是无法解决功能VB源。 Visual Basic中已经没有办法(?)来区分这些方法在调用点,除非他们有不同的参数名称,所以这样的改变可能会导致两个成员是从VB代码无法使用。
改变之前API:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
改变之后API:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
public static void Bar(ref string x) {
...
}
}
}
示例客户机代码:
Api.SomeNamespace.Foo.Bar(str)
字段来性质的变化
<强>二进制级中断/源代码级的断裂强>
除了明显的二进制电平断裂,如果构件通过引用传递到方法中,这可能会导致源代码级中断。
改变之前API:
namespace SomeNamespace {
public class Foo {
public int Bar;
}
}
改变之后API:
namespace SomeNamespace {
public class Foo {
public int Bar { get; set; }
}
}
示例客户机代码:
FooBar(ref Api.SomeNamespace.Foo.Bar);
API change:
- 增加的[废弃]属性(你有点复盖这一提的属性;然而,这可能是一个重大更改当使用预警作为错误。)
二级:
- 移动的类型从一组到另一个
- 改变名字空间的一个类型
- 增加一个基本类型是从另一个组件。
增加一个新的部件(事件保护),采用一种类型从另一个组件(2级)作为一个模板参数的约束。
protected void Something<T>() where T : Class2 { }
改变一个儿童课(Class3)从一种类型在另一个大会上课的时候是用作一个模板参数为此类。
protected class Class3 : Class2 { } protected void Something<T>() where T : Class3 { }
源水平宁静的语义的变化:
- 增加或删除/更替代的平等(),GetHashCode(),或ToString()
(不知道这些配合)
部署更改:
- 加入/消除的依赖性/参考文献
- 更新的依赖关系到较新的版本
- 变更的目标的平台'之间的x86、安腾,64,或anycpu
- 建筑的/试验在不同的框架安装(即安装3.5上。网2.0框允许API电话,然后需要。网2.0SP2)
引导/配置的变化:
- 增加或删除/更改定义结构的选项(即应用程序。配置设置)
- 与大量使用的IoC/DI在今天的应用,就有必要重新配置和/或更改启动代码DI依赖的代码。
更新:
对不起,我没有意识到,唯一的原因,这是打破对于我,我用它们在模板的制约。
添加过载的方法来消亡默认参数使用
断的种类:的源代码级的安静语义更改
由于编译器变换方法与缺少的默认参数值与在主叫侧的缺省值的显式调用呼叫,对于现有的已编译代码中给出的相容性;具有正确签名的方法将所有以前编译的代码被发现。
在另一侧,而不调用可选参数的使用现编译为到缺少可选的参数的新的方法的调用。这一切都仍然工作正常,但如果所谓的代码驻留在另一个组件,新编译的代码调用它现在是依赖于该组件的新版本。部署组件调用重构的代码而不同时部署重构的代码驻留在正在导致异常“未找到方法”的组件。
更改之前API 强>
public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
{
return mandatoryParameter + optionalParameter;
}
更改后API 强>
public int MyMethod(int mandatoryParameter, int optionalParameter)
{
return mandatoryParameter + optionalParameter;
}
public int MyMethod(int mandatoryParameter)
{
return MyMethod(mandatoryParameter, 0);
}
<强>将仍然是工作示例代码强>
public int CodeNotDependentToNewVersion()
{
return MyMethod(5, 6);
}
<强>示例代码进行编译时,现在是从属于该新版本强>
public int CodeDependentToNewVersion()
{
return MyMethod(5);
}
重命名的接口
有点儿歇的:源和二进制强>
语言影响:最有可能的所有,在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
重载方法与空类型的参数
种类:的源代码级的断裂强>
影响语言:<强> C#,VB 强>
改变之前API:
public class Foo
{
public void Bar(string param);
}
变更后API:
public class Foo
{
public void Bar(string param);
public void Bar(int? param);
}
示例客户机代码变更前的工作和之后破:
new Foo().Bar(null);
例外:呼叫是不明确以下方法或属性之间
促进到扩展方法
种类:源代码级破
语言影响:(?其它)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");
}