01

共享的BCL

虽然.NET Core借助于CoreCLR和CoreFX实现了真正的跨平台,但是目前的.NET Core仅仅提供ASP.NET Core和UWP这两种编程模型,虽然后者旨在实现多种设备的统一编程,但依然还是关注于Windows平台。对于传统.NET Framework下面向桌面应用的WPF和Windows Forms,它们并没有跨平台的意义,所以依然是今后.NET的一大分支。

除此之外,虽然我们有了跨平台的ASP.NET Core,传统的ASP.NET依然被保留了下来,并且在今后一段时间内还将继续升级。除了.NET Framework和.NET Core,.NET还具有另一个重要的分支,那就是Xamarin,它可以帮助我们为iOS、OS X和Android编写统一的应用。在.NET诞生十多年后,微软开始对.NET进行了全新的布局,建立了 “大一统” 的.NET平台。总的来说,这个所谓的大一统.NET平台由如下图所示的.NET Framework、.NET Core和Xamarin这三个分支组成。

.NET Core跨平台的奥秘[7]:全新布局[上]_java

虽然被微软重新布局的.NET平台只包含了三个分支,但是之前遇到的一个重要的问题依然存在,那就是代码的复用,说的更加具体的是应该是程序集的复用而不是源代码的复用。我们知道之前解决程序集服务的方案就是PCL,但这并不是一种理想的解决方案,由于各个目标框架具有各种独立的BCL,所以我们创建的PCL项目只能建立在指定的几种兼容目标框架的BCL交集之上。对于全新的.NET平台来说,这个问题通过提供统一的BCL得到根本的解决,这个统一的BCL被称为.NET Standard

我们可以将.NET Standard称为新一代的PCL,PCL提供的可移植能力仅仅限于创建时就确定下来的几种目标平台,但是.NET Standard做得更加彻底,因为它在设计的时候就已经考虑针对三大分支的复用。如下图所示,.NET Standard为.NET Framework、.NET Core和Xamarin提供了统一的API,那么我们在这组标准API基础上编写的代码自然就能被所有类型的.NET应用复用。

.NET Core跨平台的奥秘[7]:全新布局[上]_java_02

02

.NET Standard 

.NET Standard提供的API主要是根据现有.NET Framework来定义的,它的版本升级反映了其提供的API不断丰富的过程,目前最新版本(.NET Standard 2.0)提供的API数量在前一版本基础上几乎翻了一番。Visual Studio提供相应的项目模板帮助我们创建基于.NET Standard的类库项目,这样的项目会采用专门的目标框架别名netstandard{version}。一个针对.NET Standard 2.0的类库项目具有如下的定义,我们可以看到它采用的目标框架别名为 “.NET Standard 2.0” 。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
</Project>

顾名思义,.NET Standard仅仅是一个标准,而不提供具体的实现。我们可以简单理解为.NET Standard为我们定义了一整套标准的接口,各个分支需要针对自身的执行环境对这套接口提供实现。对于.NET Core来说,它的基础API主要由CoreFX和System.Private.CoreLib.dll这个核心程序集来承载,这些API基本上就是根据.NET Standard来设计的。

对于.NET Framework来说,它的BCL提供的API与.NET Standard存在着很大的交集,实际上.NET Standard基本上就是根据.NET Framework现有的API来设计的,所以微软不可能在.NET Framework上重写一套类型于CoreFX的实现,只需要采用某个技术 “链接” 到现有的程序集上就可以了。

03

针对.NET Standard的"链接"

一个针对.NET Standard编译生成的程序集在不同的执行环境中针对真正提供实现的程序集的所谓“链接”依然是通过上面我们介绍的“垫片”技术来实现的,为了彻底搞清楚这个问题,我们还是先来作一个简单的实例演示。如下图所示,我们创建了与上面演示实例具有类似结构的解决方案,与之不同的是,分别针对.NET Framework和.NET Core的控制台应用NetApp和NetCoreApp共同引用的类库NetStandardLib是一个.NET Standard 2.0类库项目。

与上面演示的实例一样,我们在NetStandardLib中定义了如下一个Utils类,并利用定义其中的静态方法PrintAssemblyNames数据两个数据类型(Dictionary<,>SortedDictionary<,>)所在的程序集名称,该方法分别在NetApp和NetCoreApp的入口Main方法中被调用。

NetStandardLib:

public class Utils
{
    public static void PrintAssemblyNames()
    
{           
        Console.WriteLine(typeof(Dictionary<,>).Assembly.FullName);
        Console.WriteLine(typeof(SortedDictionary<,>).Assembly.FullName);
    }
}

NetApp:

class Program
{

    static void Main()
    
{
        Console.WriteLine(".NET Framework 4.7");
        Utils.PrintAssemblyNames();
    }
}

NetCoreApp:

class Program
{

    static void Main()
    
{
        Console.WriteLine(".NET Core 2.0");
        Utils.PrintAssemblyNames();
    }
}

直接运行这两个分别针对.NET Framework和.NET Core的控制台应用NetApp和NetCoreApp,我们会发现它们会生成不同的输出结果。如下图所示,在.NET Framework和.NET Core 执行环境下,Dictionary<,>和SortedDictionary<,>这另个泛型字典类型其实来源于不同的程序集。具体来说,我们常用的Dictionary<,>类型在.NET Framework 4.7和.NET Core 2.0环境下分别定义在程序集mscorlib.dll和System.Private.CoreLib.dll中,而SortedDictionary<,>所在的程序集则分别是System.dll和System.Collection.dll。

对于演示的这个实例来说,这个NetStandardLib类库项目针对的目标框架为.NET Standard 2.0,后者最终体现为一个名为NetStandard.Library.nupkg的NuGet包,这一点其实可以从Visual Studio针对该项目的依赖节点可以看出来。如下图所示,这个名为NetStandard.Library的NuGet包具有一个核心的程序集netstandard.dll,上面我们所说的.NET Standard API就定义在该程序集中。

也就是说,所有.NET Standard 2.0项目都具有针对程序集netstandard.dll的依赖,这个依赖自然也会体现在编译后生成的程序集上。对于我们演示实例中的这个类库项目NetStandardLib编译生成的同名程序集来说,它针对程序集netstandard.dll的依赖体现在如下所示的元数据中。

.assembly extern netstandard
{
  .publickeytoken = (CC 7B 13 FF CD 2D DD 51 )                         
  .ver 2:0:0:0
}
.assembly NetStandardLib
{
  ...
}
...

按照我们既有的知识,原本定义在netstandard.dll的两个类型(Dictionary<,>和SortedDictionary<,>)在不同过的执行环境中需要被转移到另一个程序集中,我们完全可以在相应的环境中提供一个同名的垫片程序集并借助类型的跨程序集转移机制来实现,实际上微软也就是这么做的。我们先来看看针对.NET Framework的垫片程序集netstandard.dll的相关定义,我们可以直接在NetApp编译的目标目录中找到这个程序集。借助于反编译工具ildasm.exe,我们可以很容易地得到与Dictionary<,>和SortedDictionary<,>这两个泛型字典类型转移的相关元数据,具体的内容下面的代码片段所示。

.assembly extern mscorlib
{
  .publickeytoken = (B7 7556 19 34 E0 89 )                         
  .ver 0:0:0:0
}
.assembly extern System
{
  .publickeytoken = (B7 7556 19 34 E0 89 )                         
  .ver 0:0:0:0
}
.class extern forwarder System.Collections.Concurrent.ConcurrentDictionary`2
{
  .assembly extern mscorlib
}
.class extern forwarder System.Collections.Generic.SortedDictionary`2
{
  .assembly extern System
}

针对.NET Core的垫片程序集netstandard.dll被保存在我们前面提到的共享目录“%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\2.0.0”下,我们采用同样的方式提取出与Dictionary<,>和SortedDictionary<,>这两个泛型字典类型转移的元数据。从如下的代码片段我们可以清晰地看出,Dictionary<,>和SortedDictionary<,>这两个类型都被转移到程序集System.Collections.dll之中。

.assembly extern System.Collections
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 03A )                         
  .ver 0:0:0:0
}
.class extern forwarder System.Collections.Generic.Dictionary`2
{
  .assembly extern System.Collections
}
.class extern forwarder System.Collections.Generic.SortedDictionary`2
{
  .assembly extern System.Collections
}

从演示实例的执行结果我们知道,SortedDictionary<,>确实是定义在程序集System.Collections.dll中,但是我们常用的Dictionary<,>类型则出自核心程序集System.Private.CoreLib.dll,那么我们可以断定Dictionary<,>类型在System.Collections.dll中必然出现了二次转移。为了确认我们的断言,我们只需要采用相同的方式反编译程序集System.Collections.dll,该程序集也被存储在共享目录 “%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\2.0.0” 中,该程序集中针对Dictionary<,>类型的转移体现在如下所示的元数据中。

.assembly extern System.Private.CoreLib
{
  .publickeytoken = (7C EC 85 D7 BE A7 79 8E )                         
  .ver 4:0:0:0
}
.class extern forwarder System.Collections.Generic.Dictionary`2
{
  .assembly extern System.Private.CoreLib
}

上面针对Dictionary<,>和SortedDictionary<,>这两个类型分别在.NET Framework 4.7和.NET Core环境下的跨程序集转移路径基本上体现在下图之中。简单来说,.NET Framework环境下的垫片程序集netstandard.dll将这两个类型分别转移到了程序集mscorlib.dll和System.dll之中。如果执行环境切换到了.NET Core,这两个类型先被转移到System.Collection.dll中,但是Dictionary<,>这个常用类型最终是由System.Private.CoreLib.dll这个基础程序集承载的,所有System.Collection.dll中针对该类型作了二次转移。

.NET Core跨平台的奥秘[7]:全新布局[上]_java_03



04

总结

上面这个简单的类型基本上揭示了.NET Standard为什么能够提供全平台的可移植性,我们现在来对此做一个简单的总结。.NET Standard API由NetStandard.Library这个NuGet包来承载,后者提供了一个名为netstandard.dll的程序集,保留在这个程序集中的仅仅是. NET Standard API的存根(Stub),而不提供具体的实现。所有对于一个目标框架为.NET Standard的类库项目编译生成的程序集来说,它们保留了针对程序集netstandard.dll的引用。

.NET平台的三大分支(.NET Framework、.NET Core和Xamarin)按照自己的方式各自实现了.NET Standard规定的这套标准的API。由于在运行时真正承载.NET Standard API的类型被分布到多个程序集中,所以. NET Standard程序集能够被复用的前提是运行时能够将这些基础类型链接到对应的程序集上。由于. NET Standard程序集是针对netstandard.dll进行编译的,所以我们只需要在各自环境中提供这个同名的程序集来完成类型的转移即可。

https://mp.weixin.qq.com/s/2fGfWfMqEIGiA_dC9OpKuQ