.NET库和向后兼容的技巧——第3部分

目录

二进制不兼容

害怕承诺

优点

缺点

强大的命名难题

二进制不兼容的类型

二进制兼容性和源代码兼容性

怎么不发疯

所有美好的事物都必须走到尽头


这是.NET库和向后兼容系列技术的第三篇文章:

本系列的第1部分和第2部分将讨论如何通过更改行为(行为不兼容)或导致编译错误(源代码不兼容)来以不会破坏客户应用程序的方式更新库的代码。行为上的不兼容是偷偷摸摸的,必须不惜一切代价避免。源代码的不兼容是客户要解决的难题,应将其最小化。

二进制不兼容

当用户通过将.dll文件放入应用程序文件夹而不重新编译应用程序本身来更新您的库时,会发生第三种不兼容性。该更新可以由应用程序的作者甚至最终用户执行。

害怕承诺

您需要做的第一件事是确定是否应允许这种形式的更新。允许和禁止它都有优点和缺点。您的选择将影响您编写库的方式和文档编制方式,因此您应尽早决定。

优点

  • 特别是对于安全补丁,最终用户可以更新您的库,而不必等待应用程序的作者发布新版本。
  • 如果您的库被广泛使用,则有人可以编写具有两个依赖关系的应用程序,这些依赖关系使用您库的不同版本。如果您不允许两个依赖项透明地使用较新版本,则会出现问题

缺点

  • 确保二进制兼容性非常困难,因此如果您不小心,很可能会违背诺言
  • 在客户更新您的库时,强迫客户重建他们的应用程序将导致他们运行测试,这些测试可能会捕获行为不兼容性。您甚至可以愿意引入源代码不兼容的情况,以迫使客户解决库行为的变化
  • 不保证二进制兼容性将使您在设计库的新版本时拥有更大的自由度,并且随着时间的推移可能会带来更好的用户体验

强大的命名难题

二进制兼容性仅在库的.dll文件可以用较新版本替换时才有用,否则,行为和源代码兼容性都是您所需要担心的!值得一提的是,对库的强命名可能会阻止用户用较新的版本替换它。

.NET的一个令人困惑的功能,包括使用密码密钥对程序集进行签名,该程序集根据其名称和版本为其分配一个唯一的标识。

足够奇怪的是,即使涉及加密,也不应依赖强命名来保证安全性。

点击这里查看微软的指导。

如果您不强命名您的库,那么您是清白的。只是知道,如果潜在客户想强行命名其程序集,将无法使用您的库。

如果要强命名库,通常的方法是保持程序集版本不变,除非您确实要进行重大更改。 

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
 
    <Version>1.0.1</Version>
    <FileVersion>1.0.1</FileVersion>
    <!-- Don't increase the AssemblyVersion unless you are making breaking changes-->
    <AssemblyVersion>1.0.0</AssemblyVersion>
     
    <SignAssembly>true</SignAssembly>
    <AssemblyOriginatorKeyFile>SNKey.pfx</AssemblyOriginatorKeyFile>
  </PropertyGroup>
</Project>

即使程序集是强命名的,这也允许所有不同版本可以互换。Microsoft本身意识到这是令人困惑和麻烦的,并更改了.NET Core中严格的程序集版本加载,使其更加轻松。如果您的库以.NET Standard为目标,则将根据.NET Framework.NET Core使用程序集加载规则,具体取决于哪个应用程序使用它。

二进制不兼容的类型

二进制不兼容有两种类型:导致异常的二进制不兼容和导致行为更改的二进制不兼容。

由二进制不兼容引起的典型异常是TypeLoadExceptionMissingMethodException。它们特别难以捕获,因为在CLR首次尝试从您的库中访问受影响的类型或成员时会抛出它们,该时间早于首次引用该类型或成员的实际代码行。

与二进制不兼容相关的行为更改与常规行为不兼容有所不同,因为可以通过重新编译使用您的库的代码来解决这些问题。这可能会使用户感到困惑,因为用户可能会尝试在其应用程序的新编译调试版本上重现该问题,并且不会受到影响。

一个有趣的示例是重新排列枚举的条目。由于.NET会自动为枚举条目分配一个数值,并且在编译时将此值嵌入到使用的程序集中,因此对枚举重新排序既会导致由于二进制不兼容而导致的行为更改,以及在重新编译应用程序时发生的不同的行为变化!

以下代码 

static void Main(string[] args)
{
  Console.WriteLine(
    $"This is Enum1.a: '{Enum1.a}'. It's value is 0: '{(int)Enum1.a}'.");
}
 
//This must be in a separate library
public enum Enum1
{
  a, b
}

通常会打印

This is Enum1.a: 'a'. It's value is 0: '0'.

如果我们将库中的枚举定义更改为

public enum Enum1
{
  b, a
}

该应用程序现在将打印

This is Enum1.a: 'b'. It's value is 0: '0'.

这是因为Enum1.a在应用程序的程序集中被编译为0。因此,当我们切换到新库而不进行重新编译时,将保留0值,但现在它对应于Enum1.b

如果我们重新编译应用程序,那么我们现在将有第三种不同的行为!

This is Enum1.a: 'a'. It's value is 0: '1'.

二进制兼容性和源代码兼容性

可能会认为所有二进制不兼容性,至少是导致TypeLoadExceptionMissingMethodException二进制不兼容也是源代码不兼容性。这不是真的。

以下是源代码兼容但二进制不兼容的代码更改列表。

之前

public class Class1
{
  public static void F()
  {
    Console.WriteLine("1");
  }
}

之后

public class Class1
{
  //Adding a parameter with a default
  //results in MissingMethodException.
  //Create an overloaded method instead.
  public static void F(int n = 1)
  {
    Console.WriteLine(n);
  }
}

之前

public class Class1
{
  public int Number = 0;
}

之后

public class Class1
{
  //Changing a field into a property will
  //results in MissingFieldException
  public int Number { get; set; }  = 0;
}

之前

public interface IFoo
{
  void F();
}

之后

public interface IFooBase
{
  void F();
}
 
//Moving an interface member to a base
//interface results in
//MissingMethodException
public interface IFoo : IFooBase
{
}

大多数源代码不兼容也是二进制不兼容。很少有例外。

之前

public class Class1
{
  public static void F(int n)
  {
    Console.WriteLine(n);
  }
}

之后

public class Class1
{
  //Changing parameter names break
  //compilation if your customer uses
  //named arguments
  public static void F(int x)
  {
    Console.WriteLine(x);
  }
}

某些行为更改仅在重新编译时生效。

之前

public class Class1
{
  public static void F(int n = 1)
  {
    Console.WriteLine(n);
  }
}

之后

public class Class1
{
  //Default values are embedded in the
  //calling assembly so this change
  //require recompilation to show an
  //effect
  public static void F(int n = 2)
  {
    Console.WriteLine(n);
  }
}

之前

public class Class1
{
  public static void Print(object o)
  {
    Console.WriteLine(o);
  }
}

之后

public class Class1
{
  public static void Print(object o)
  {
    Console.WriteLine(o);
  }
  //The new overload would be used only
  //by an application compiled against
  //the new library
  public static void Print(object[] o)
  {
    Console.WriteLine(
      string.Join("; ", o));
  }
}

怎么不发疯

由于二进制兼容性和源代码兼容性之间的关系是如此复杂,因此我强烈建议您:

  1. 根本不保证二进制兼容性
  2. 或保证二进制和源代码兼容性。

现在是时候回去重新阅读文章开头的优点/缺点部分了。

好消息是,尽管永远不能完全保证源代码兼容性(请参阅第2部分),但实际上二进制文件兼容性是完全可以实现的。坏消息是,根本不知道什么是二进制兼容的,什么不是!

幸运的是,测试更改的类型是否向后兼容非常容易:

  1. 用两个项目创建一个解决方案:一个应用程序和一个类库
  2. 在应用程序项目中添加对类库的引用
  3. 尽可能减少代码量,以在库和应用程序中重现用例
  4. 生成解决方案,测试程序是否按预期工作,并备份应用程序的bin/Debug文件夹
  5. 进行更改以测试其兼容性
  6. 构建解决方案,测试程序是否正常运行
  7. 仅将类库.dll文件而不是应用程序的.exe复制到步骤4中创建的备份文件夹中。
  8. 从备份文件夹中运行该应用程序,并验证其是否仍然正常运行。

例如,通过测试以下内容,我们可以轻松地验证将方法移至基类是二进制兼容的(我不会猜到)。

之前

/In the application
class Program
{
  static void Main(string[] args)
  {
    new Foo().DoSomething();
    Console.WriteLine("Press ENTER");
    Console.ReadLine();
  }
}
//In the class library
public class Foo
{
  public void DoSomething()
  {
    Console.WriteLine("Something");
  }
}

之后

//In the application
class Program
{
  static void Main(string[] args)
  {
    new Foo().DoSomething();
    Console.WriteLine("Press ENTER");
    Console.ReadLine();
  }
}
//In the class library
public class Foo: FooBase
{
}
public class FooBase
{
  public void DoSomething()
  {
    Console.WriteLine("Something");
  }
}

所有美好的事物都必须走到尽头

好了,这就是这个冗长的系列的结尾。

在过去的几年中,保持库与数十万用户的向后兼容性一直是我的主要关注之一。我确信我还没有学到关于该主题的所有知识,但是我衷心希望这对其他.NET开发人员是有用的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值