.NET 中 API 重大变化的权威指南

发布于 2024-08-05 04:31:42 字数 2680 浏览 2 评论 0原文

我想收集尽可能多的有关 .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# 并未损坏,因为它没有对 ExtensionMethodAttributeExtensionMethodAttribute< 的语言级别支持/code>,并且要求 CLS 扩展方法作为静态方法调用。

I would like to gather as much information as possible regarding API versioning in .NET/CLR, and specifically how API changes do or do not break client applications. First, let's define some terms:

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).

Binary-level break - an API change that results in client assemblies compiled against older version of the API potentially not loading with the new version. Example: changing method signature, even if it allows to be called in the same way as before (ie: void to return type / parameter default values overloads).

Source-level break - an API change that results in existing code written to compile against older version of the API potentially not compiling with the new version. Already compiled client assemblies work as before, however. Example: adding a new overload that can result in ambiguity in method calls that were unambiguous previous.

Source-level quiet semantics change - an API change that results in existing code written to compile against older version of the API quietly change its semantics, e.g. by calling a different method. The code should however continue to compile with no warnings/errors, and previously compiled assemblies should work as before. Example: implementing a new interface on an existing class that results in a different overload being chosen during overload resolution.

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.

A few examples to kickstart this:

Adding new method overloads

Kind: source-level break

Languages affected: C#, VB, F#

API before change:

public class Foo
{
    public void Bar(IEnumerable x);
}

API after change:

public class Foo
{
    public void Bar(IEnumerable x);
    public void Bar(ICloneable x);
}

Sample client code working before change and broken after it:

new Foo().Bar(new int[0]);

Adding new implicit conversion operator overloads

Kind: source-level break.

Languages affected: C#, VB

Languages not affected: F#

API before change:

public class Foo
{
    public static implicit operator int ();
}

API after change:

public class Foo
{
    public static implicit operator int ();
    public static implicit operator float ();
}

Sample client code working before change and broken after it:

void Bar(int x);
void Bar(float x);
Bar(new Foo());

Notes: F# is not broken, because it does not have any language level support for overloaded operators, neither explicit nor implicit - both have to be called directly as op_Explicit and op_Implicit methods.

Adding new instance methods

Kind: source-level quiet semantics change.

Languages affected: C#, VB

Languages not affected: F#

API before change:

public class Foo
{
}

API after change:

public class Foo
{
    public void Bar();
}

Sample client code that suffers a quiet semantics change:

public static class FooExtensions
{
    public void Bar(this Foo foo);
}

new Foo().Bar();

Notes: F# is not broken, because it does not have language level support for ExtensionMethodAttribute, and requires CLS extension methods to be called as static methods.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(18

注定孤独终老 2024-08-12 04:31:42

更改方法签名

种类:二进制级中断

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

Changing a method signature

Kind: Binary-level Break

Languages affected: C# (VB and F# most likely, but untested)

API before change

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

API after change

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

Sample client code working before change

Foo.bar(13);
冰魂雪魄 2024-08-12 04:31:42

添加具有默认值的参数。

中断类型:二进制级中断

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

这是因为 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# 中的编译时常量)。

Adding a parameter with a default value.

Kind of Break: Binary-level break

Even if the calling source code doesn't need to change, it still needs to be recompiled (just like when adding a regular parameter).

That is because C# compiles the default values of the parameters directly into the calling assembly. It means that if you don't recompile, you will get a MissingMethodException because the old assembly tries to call a method with less arguments.

API Before Change

public void Foo(int a) { }

API After Change

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

Sample client code that is broken afterwards

Foo(5);

The client code needs to be recompiled into Foo(5, null) at the bytecode level. The called assembly will only contain Foo(int, string), not Foo(int). That's because default parameter values are purely a language feature, the .Net runtime does not know anything about them. (This also explain why default values have to be compile-time constants in C#).

沦落红尘 2024-08-12 04:31:42

当我发现它时,这一点非常不明显,尤其是考虑到与接口相同情况的差异。这根本不是休息,但令人惊讶的是我决定将其包括在内:

将类成员重构为基类

种类:不是休息!

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

更改前的 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 是一个类这一事实才是造成差异的原因。去算算...

This one was very non-obvious when I discovered it, especially in light of the difference with the same situation for interfaces. It's not a break at all, but it's surprising enough that I decided to include it:

Refactoring class members into a base class

Kind: not a break!

Languages affected: none (i.e. none are broken)

API before change:

class Foo
{
    public virtual void Bar() {}
    public virtual void Baz() {}
}

API after change:

class FooBase
{
    public virtual void Bar() {}
}

class Foo : FooBase
{
    public virtual void Baz() {}
}

Sample code that keeps working throughout the change (even though I expected it to break):

// C++/CLI
ref class Derived : Foo
{
   public virtual void Baz() {{

   // Explicit override    
   public virtual void BarOverride() = Foo::Bar {}
};

Notes:

C++/CLI is the only .NET language that has a construct analogous to explicit interface implementation for virtual base class members - "explicit override". I fully expected that to result in the same kind of breakage as when moving interface members to a base interface (since IL generated for explicit override is the same as for explicit implementation). To my surprise, this is not the case - even though generated IL still specifies that BarOverride overrides Foo::Bar rather than FooBase::Bar, assembly loader is smart enough to substitute one for another correctly without any complaints - apparently, the fact that Foo is a class is what makes the difference. Go figure...

终陌 2024-08-12 04:31:42

这可能是“添加/删除接口成员”的一个不太明显的特殊情况,我认为根据我接下来要发布的另一个案例,它应该有自己的条目。因此:

将接口成员重构为基本接口

种类:在源代码和二进制级别上中断

受影响的语言: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)将在源代码和二进制级别上正常工作。方法调用也不会中断。

This one is a perhaps not-so-obvious special case of "adding/removing interface members", and I figured it deserves its own entry in light of another case which I'm going to post next. So:

Refactoring interface members into a base interface

Kind: breaks at both source and binary levels

Languages affected: C#, VB, C++/CLI, F# (for source break; binary one naturally affects any language)

API before change:

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

API after change:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Sample client code that is broken by change at source level:

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

Sample client code that is broken by change at binary level;

(new Foo()).Bar();

Notes:

For source level break, the problem is that C#, VB and C++/CLI all require exact interface name in the declaration of interface member implementation; thus, if the member gets moved to a base interface, the code will no longer compile.

Binary break is due to the fact that interface methods are fully qualified in generated IL for explicit implementations, and interface name there must also be exact.

Implicit implementation where available (i.e. C# and C++/CLI, but not VB) will work fine on both source and binary level. Method calls do not break either.

看春风乍起 2024-08-12 04:31:42

重新排序枚举值

中断类型:源级/二进制级安静语义更改

受影响的语言:所有

重新排序枚举值将保持源级兼容性,因为文字具有相同的名称,但它们的序数索引将是已更新,这可能会导致某些无提示的源代码级别中断。

更糟糕的是,如果客户端代码未针对新的 API 版本重新编译,则可能会引入无提示的二进制级别中断。枚举值是编译时常量,因此它们的任何使用都会被纳入客户端程序集的 IL 中。这种情况有时特别难以发现。

API 更改之前

public enum Foo
{
   Bar,
   Baz
}

API 更改之后 可以正常

public enum Foo
{
   Baz,
   Bar
}

工作但之后损坏的示例客户端代码:

Foo.Bar < Foo.Baz

Reordering enumerated values

Kind of break: Source-level/Binary-level quiet semantics change

Languages affected: all

Reordering enumerated values will keep source-level compatibility as literals have the same name, but their ordinal indices will be updated, which can cause some kinds of silent source-level breaks.

Even worse is the silent binary-level breaks that can be introduced if client code is not recompiled against the new API version. Enum values are compile-time constants and as such any uses of them are baked into the client assembly's IL. This case can be particularly hard to spot at times.

API Before Change

public enum Foo
{
   Bar,
   Baz
}

API After Change

public enum Foo
{
   Baz,
   Bar
}

Sample client code that works but is broken afterwards:

Foo.Bar < Foo.Baz
泛泛之交 2024-08-12 04:31:42

这在实践中确实是一件非常罕见的事情,但当它发生时仍然令人惊讶。

添加新的非重载成员

种类:源代码级别中断或安静语义更改。

受影响的语言: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 中存在 lambda 类型推断引起的过载决议。这里采用鸭子类型的有限形式来打破多个类型匹配的关系,方法是检查 lambda 的主体对于给定类型是否有意义 - 如果只有一种类型产生可编译的主体,则选择该类型。

这里的危险是客户端代码可能有一个重载的方法组,其中一些方法采用自己类型的参数,而其他方法则采用您的库公开的类型的参数。如果他的任何代码依赖类型推断算法仅根据成员的存在或不存在来确定正确的方法,那么向您的类型之一添加与客户端类型之一同名的新成员可能会引发推断关闭,导致重载决策期间出现歧义。

请注意,此示例中的类型 FooBar 不以任何方式相关,无论是通过继承还是其他方式。仅在单个方法组中使用它们就足以触发这种情况,如果这种情况发生在客户端代码中,您将无法控制它。

上面的示例代码演示了一种更简单的情况,其中这是源代码级别的中断(即编译器错误结果)。然而,如果通过推理选择的重载具有其他参数,这也可能是无声的语义更改,否则会导致其排名靠后(例如,具有默认值的可选参数,或者需要隐式声明和实际参数之间的类型不匹配)转换)。在这种情况下,重载决策将不再失败,但编译器将悄悄选择不同的重载。然而,在实践中,如果不仔细构建方法签名来故意导致这种情况,就很难遇到这种情况。

This one is really a very rare thing in practice, but nonetheless a surprising one when it happens.

Adding new non-overloaded members

Kind: source level break or quiet semantics change.

Languages affected: C#, VB

Languages not affected: F#, C++/CLI

API before change:

public class Foo
{
}

API after change:

public class Foo
{
    public void Frob() {}
}

Sample client code that is broken by change:

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());        
    }
}

Notes:

The problem here is caused by lambda type inference in C# and VB in presence of overload resolution. A limited form of duck typing is employed here to break ties where more than one type matches, by checking whether the body of the lambda makes sense for a given type - if only one type results in compilable body, that one is chosen.

The danger here is that client code may have an overloaded method group where some methods take arguments of his own types, and others take arguments of types exposed by your library. If any of his code then relies on type inference algorithm to determine the correct method based solely on presence or absence of members, then adding a new member to one of your types with the same name as in one of the client's types can potentially throw inference off, resulting in ambiguity during overload resolution.

Note that types Foo and Bar in this example are not related in any way, not by inheritance nor otherwise. Mere use of them in a single method group is enough to trigger this, and if this occurs in client code, you have no control over it.

The sample code above demonstrates a simpler situation where this is a source-level break (i.e. compiler error results). However, this can also be a silent semantics change, if the overload that was chosen via inference had other arguments which would otherwise cause it to be ranked below (e.g. optional arguments with default values, or type mismatch between declared and actual argument requiring an implicit conversion). In such scenario, the overload resolution will no longer fail, but a different overload will be quietly selected by the compiler. In practice, however, it is very hard to run into this case without carefully constructing method signatures to deliberately cause it.

总攻大人 2024-08-12 04:31:42

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

中断类型:

受影响的源语言和二进制语言:全部

这实际上只是更改方法可访问性的一种变体 - 它只是更微妙一些,因为很容易忽视这样一个事实:并非所有对接口方法的访问都必须通过引用接口的类型。

更改前的 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

Convert an implicit interface implementation into an explicit one.

Kind of Break: Source and Binary

Languages Affected: All

This is really just a variation of changing a method's accessibility - its just a little more subtle since it's easy to overlook the fact that not all access to an interface's methods are necessarily through a reference to the type of the interface.

API Before Change:

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

API After Change:

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

Sample Client code that works before change and is broken afterwards:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
手心的海 2024-08-12 04:31:42

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

中断类型:

受影响的源语言:全部

将显式接口实现重构为隐式接口实现在破坏 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"

Convert an explicit interface implementation into an implicit one.

Kind of Break: Source

Languages Affected: All

The refactoring of an explicit interface implementation into an implicit one is more subtle in how it can break an API. On the surface, it would seem that this should be relatively safe, however, when combined with inheritance it can cause problems.

API Before Change:

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

API After Change:

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

Sample Client code that works before change and is broken afterwards:

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"
悲凉≈ 2024-08-12 04:31:42

将字段更改为属性

中断类型:

受影响的 API 语言:Visual Basic 和 C#*

信息:当您将普通字段或变量更改为 Visual Basic 中的属性时,以任何方式引用该成员的任何外部代码都需要重新编译。

更改前的 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"

Changing a field to a property

Kind of Break: API

Languages Affected: Visual Basic and C#*

Info: When you change a normal field or variable into a property in visual basic, any outside code referencing that member in any way will need to be recompiled.

API Before Change:

Public Class Foo    
    Public Shared Bar As String = ""    
End Class

API After Change:

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    

Sample client code that works but is broken afterwards :

Foo.Bar = "foobar"
对你的占有欲 2024-08-12 04:31:42

命名空间添加

源级中断/源级安静语义更改

由于命名空间解析在 vb.Net 中的工作方式,向库添加命名空间可能会导致使用以前版本的 Visual Basic 代码进行编译API 不使用新版本进行编译。

示例客户端代码:

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 在其 Api.SomeNamespace.Data 命名空间中包含类 DataRow,则代码将编译,但 dr 将是一个实例使用旧版本 API 编译时的 System.Data.DataRow 和使用新版本 API 编译时的 Api.SomeNamespace.Data.DataRow

参数重命名

源级中断

更改参数名称是 vb.net 从版本 7(?)(.Net 版本 1?)和 c#.net 从版本 4(.Net 版本)的重大更改4).

更改前的 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 参数

源级中断

添加具有相同签名的方法重写(除了一个参数通过引用而不是通过值传递)将导致 vb 源引用API无法解析该函数​​。 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);

Namespace Addition

Source-level break / Source-level quiet semantics change

Due to the way namespace resolution works in vb.Net, adding a namespace to a library can cause Visual Basic code that compiled with a previous version of the API to not compile with a new version.

Sample client code:

Imports System
Imports Api.SomeNamespace

Public Class Foo
    Public Sub Bar()
        Dim dr As Data.DataRow
    End Sub
End Class

If a new version of the API adds the namespace Api.SomeNamespace.Data, then the above code will not compile.

It becomes more complicated with project-level namespace imports. If Imports System is omitted from the above code, but the System namespace is imported at the project level, then the code may still result in an error.

However, if the Api includes a class DataRow in its Api.SomeNamespace.Data namespace, then the code will compile but dr will be an instance of System.Data.DataRow when compiled with the old version of the API and Api.SomeNamespace.Data.DataRow when compiled with the new version of the API.

Argument Renaming

Source-level break

Changing the names of arguments is a breaking change in vb.net from version 7(?) (.Net version 1?) and c#.net from version 4 (.Net version 4).

API before change:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API after change:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string y) {
           ...
        }
    }
}

Sample client code:

Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB

Ref Parameters

Source-level break

Adding a method override with the same signature except that one parameter is passed by reference instead of by value will cause vb source that references the API to be unable to resolve the function. Visual Basic has no way(?) to differentiate these methods at the call point unless they have different argument names, so such a change could cause both members to be unusable from vb code.

API before change:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API after change:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
        public static void Bar(ref string x) {
           ...
        }
    }
}

Sample client code:

Api.SomeNamespace.Foo.Bar(str)

Field to Property Change

Binary-level break/Source-level break

Besides the obvious binary-level break, this can cause a source-level break if the member is passed to a method by reference.

API before change:

namespace SomeNamespace {
    public class Foo {
        public int Bar;
    }
}

API after change:

namespace SomeNamespace {
    public class Foo {
        public int Bar { get; set; }
    }
}

Sample client code:

FooBar(ref Api.SomeNamespace.Foo.Bar);
ぃ弥猫深巷。 2024-08-12 04:31:42

API 更改:

  1. 添加 [Obsolete] 属性(您有点通过提及属性来覆盖这一点;但是,当使用 warning-as-error 时,这可能是一个重大更改。)

二进制级别中断:

  1. 将类型从一个程序集移动到另一个
  2. 程序集 更改类型的命名空间
  3. 从另一程序集添加基类类型。
  4. 添加一个新成员(受事件保护),该成员使用另一个程序集 (Class2) 中的类型作为模板参数约束。

    protected void Something() 其中 T : Class2 { }
    
  5. 当子类 (Class3) 用作此类的模板参数时,将其更改为从另一个程序集中的类型派生。

    受保护的类 Class3 : Class2 { }
    protected void Something() 其中 T : Class3 { }
    

源级安静语义更改:

  1. 添加/删除/更改 Equals()、GetHashCode() 或 ToString() 的重写

(不确定这些内容适合什么位置)

部署更改:

  1. 添加/删除依赖项/引用
  2. 将依赖项更新到较新版本
  3. 在 x86、Itanium、x64 或任何 CPU 之间更改“目标平台”
  4. 在不同的框架安装上构建/测试(即在 .Net 2.0 机器上安装 3.5 允许 API 调用,然后需要.Net 2.0 SP2)

引导/配置更改:

  1. 添加/删除/更改自定义配置选项(即 App.config 设置)
  2. 随着当今应用程序中 IoC/DI 的大量使用,有必要重新配置和/或更改 DI 相关代码的引导代码。

更新:

抱歉,我没有意识到这对我来说是破坏的唯一原因是我在模板约束中使用了它们。

API change:

  1. Adding the [Obsolete] attribute (you kinda covered this with mentioning attributes; however, this can be a breaking change when using warning-as-error.)

Binary-level break:

  1. Moving a type from one assembly to another
  2. Changing the namespace of a type
  3. Adding a base class type from another assembly.
  4. Adding a new member (event protected) that uses a type from another assembly (Class2) as a template argument constraint.

    protected void Something<T>() where T : Class2 { }
    
  5. Changing a child class (Class3) to derive from a type in another assembly when the class is used as a template argument for this class.

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }
    

Source-level quiet semantics change:

  1. Adding/removing/changing overrides of Equals(), GetHashCode(), or ToString()

(not sure where these fit)

Deployment changes:

  1. Adding/removing dependencies/references
  2. Updating dependencies to newer versions
  3. Changing the 'target platform' between x86, Itanium, x64, or anycpu
  4. Building/testing on a different framework install (i.e. installing 3.5 on a .Net 2.0 box allows API calls that then require .Net 2.0 SP2)

Bootstrap/Configuration changes:

  1. Adding/Removing/Changing custom configuration options (i.e. App.config settings)
  2. With the heavy use of IoC/DI in todays applications, it's somethings necessary to reconfigure and/or change bootstrapping code for DI dependent code.

Update:

Sorry, I didn't realize that the only reason this was breaking for me was that I used them in template constraints.

还如梦归 2024-08-12 04:31:42

添加重载方法以消除默认参数使用

中断类型:源级安静语义更改

由于编译器将缺少默认参数值的方法调用转换为调用方具有默认值的显式调用,因此兼容性给出了现有编译代码;将为所有先前编译的代码找到具有正确签名的方法。

另一方面,不使用可选参数的调用现在被编译为对缺少可选参数的新方法的调用。一切仍然工作正常,但如果被调用的代码驻留在另一个程序集中,则调用它的新编译的代码现在依赖于该程序集的新版本。部署调用重构代码的程序集而不同时部署重构代码所在的程序集会导致“找不到方法”异常。

更改前的 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); 
  }

Adding overload methods to demise default parameters usage

Kind of break: Source-level quiet semantics change

Because the compiler transforms method calls with missing default parameter values to an explicit call with the default value on the calling side, compatibility for existing compiled code is given; a method with the correct signature will be found for all previously compiled code.

On the other side, calls without usage of optional parameters are now compiled as a call to the new method that is missing the optional parameter. It all is still working fine, but if the called code resides in another assembly, newly compiled code calling it is now dependent to the new version of this assembly. Deploying assemblies calling the refactored code without also deploying the assembly the refactored code resides in is resulting in "method not found" exceptions.

API before change

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

API after change

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

Sample code that will still be working

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

Sample code that is now dependent to the new version when compiling

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }
樱娆 2024-08-12 04:31:42

重命名接口

有点破坏:源代码和二进制

受影响的语言:很可能全部,在 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

Renaming an interface

Kinda of Break: Source and Binary

Languages Affected: Most likely all, tested in C#.

API Before Change:

public interface IFoo
{
    void Test();
}

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

API After Change:

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

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

Sample client code that works but is broken afterwards:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
酷到爆炸 2024-08-12 04:31:42

升级到扩展方法

种类:源代码级中断

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

Promotion to an Extension Method

Kind: source-level break

Languages affected: C# v6 and higher (maybe others?)

API before change:

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

API after change:

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

Sample client code working before change and broken after it:

using static Foo;

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

More Info: https://github.com/dotnet/csharplang/issues/665

a√萤火虫的光℡ 2024-08-12 04:31:42

具有可为 null 类型的参数的重载方法

种类:源代码级别中断

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

异常:以下方法或属性之间的调用不明确。

Overloading method with a parameter of nullable type

Kind: Source-level break

Languages affected: C#, VB

API before a change:

public class Foo
{
    public void Bar(string param);
}

API after the change:

public class Foo
{
    public void Bar(string param);
    public void Bar(int? param);
}

Sample client code working before the change and broken after it:

new Foo().Bar(null);

Exception: The call is ambiguous between the following methods or properties.

我喜欢麦丽素 2024-08-12 04:31:42

Visual Studio 扩展 NDepend 在 API 重大更改类别中提供了多个规则来检测二进制级别中断。仅当定义了 NDepend 基线 时,才会执行这些规则。

  • API 重大更改:类型:此规则如果基线中公开可见的类型不再公开可见或者已被删除,则会发出警告。使用此类类型的客户端代码将被破坏。
  • API 重大更改:方法:此规则如果基线中公开可见的方法不再公开可见或者已被删除,则会发出警告。使用这种方法的客户端代码将会被破坏。请注意,如果方法签名发生更改,旧方法版本将被视为已删除,而新方法版本将被视为已添加,因此将在旧方法版本上检测到重大更改。
  • API 重大更改:字段:此规则如果基线中公开可见的字段不再公开可见或者已被删除,则会发出警告。使用此类字段的客户端代码将被破坏。
  • API 重大更改:接口和抽象类 :如果公开可见的接口或抽象类已更改并包含新的抽象方法,或者某些抽象方法已被删除,则此规则会发出警告。实现此类接口或从此类抽象类派生的客户端代码将被破坏。
  • 损坏的可序列化类型:此规则发出警告使用 SerializedAttribute 标记的类型发生重大变化。为此,此规则搜索自基线以来添加或删除的可序列化实例字段的可序列化类型。请注意,它不考虑用 NonSerializedAttribute 标记的字段。
  • 避免更改枚举标志状态:此规则与基线中过去用 FlagsAttribute 标记的枚举类型匹配,现在不再匹配。它还匹配相反的枚举类型,这些类型现在用 FlagsAttribute 标记,并且未在基线中标记。使用 FlagsAttribute 标记是枚举的一个强大属性。与其说是在行为方面(当枚举被标记为 FlagsAttribute 时,只有 enum.ToString() 方法行为发生变化),而是在含义方面:枚举是值的范围或标志的范围?

另外还建议使用 3 个代码查询来让用户浏览新的公共 API 元素:

The Visual Studio Extension NDepend provides several rules in the category API Breaking Changes to detect binary level break. These rules are executed only if the NDepend baseline is defined.

  • API Breaking Changes: Types: This rule warns if a type publicly visible in the baseline, is not publicly visible anymore or if it has been removed. Clients code using such type will be broken.
  • API Breaking Changes: Methods: This rule warns if a method publicly visible in the baseline, is not publicly visible anymore or if it has been removed. Clients code using such method will be broken. Note that if a method signature gets changed the old method version is seen as removed and the new method version is seen as added, so a breaking change will be detected on the old method version.
  • API Breaking Changes: Fields: This rule warns if a field publicly visible in the baseline, is not publicly visible anymore or if it has been removed. Clients code using such field will be broken.
  • API Breaking Changes: Interfaces and Abstract Classes: This rule warns if a publicly visible interface or abstract class has been changed and contains new abstract methods or if some abstract methods have been removed. Clients code that implement such interface or derive from such abstract class will be broken.
  • Broken serializable types: This rule warns about breaking changes in types tagged with SerializableAttribute. To do so, this rule searches for serializable type with serializable instance fields added or removed since the baseline. Notice that it doesn't take account of fields tagged with NonSerializedAttribute.
  • Avoid changing enumerations Flags status: This rule matches enumeration types that used to be tagged with FlagsAttribute in the baseline, and not anymore. It also matches the opposite, enumeration types that are now tagged with FlagsAttribute, and were not tagged in the baseline. Being tagged with FlagsAttribute is a strong property for an enumeration. Not so much in terms of behavior (only the enum.ToString() method behavior changes when an enumeration is tagged with FlagsAttribute) but in terms of meaning: is the enumeration a range of values or a range of flags?

Also 3 code queries are proposed to let the user browse new public API elements:

橘寄 2024-08-12 04:31:42

静态只读转换为 const

类型:二进制级中断

受影响的语言: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被抛出。

Static readonly conversion to const

Kind: Binary-level Break

Languages affected: C#, VB, and F#

API before change:

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

API after change:

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

All clients need to be recompiled to target the new change, otherwise a MissingFieldException is thrown.

感情洁癖 2024-08-12 04:31:42

.NET 的文档有一个关于此主题的精彩页面,更改兼容性规则
我相信它涵盖了几乎所有二进制、源代码甚至行为破坏性更改。

.NET's documentation has a great page on this topic, Change rules for compatibility.
I believe it covers pretty much all binary, source and even behavioral breaking changes.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文