2.3 全新的布局
从本质上讲,按照 CLI规范设计的.NET从其诞生的那一刻就具有跨平台的“基因”,它与Java 没有本质区别。由于采用了统一的中间语言,所以只需要针对不同的平台设计不同的虚拟机(运行时)就能弥补不同操作系统与处理器架构之间的差异,但是理想与现实的差距非常大。在过去的十多年中,微软将.NET引入各个不同的应用领域,并且采用完全独立的多目标框架的设计思路,所以针对多目标框架的代码复用只能通过 PCL 这种“妥协”的方式来解决。如果依旧按照这条道路走下去,.NET的触角延伸得越广,枷锁就越多,所以.NET已经到了不得不做出彻底改变的时刻。
2.3.1 跨平台的.NET Core
综上所述,要真正实现.NET 的跨平台,需要解决两个问题:一是针对不同的平台设计相应的运行时,从而为中间语言 CIL提供一致性的执行环境;二是提供统一的 BCL,从而彻底解决代码复用的难题。对于真正跨平台的.NET Core 来说,微软不仅为它设计了针对不同平台被称为CoreCLR的运行时,还重新设计了一套被称为CoreFX的BCL。
如图2-21所示,.NET Core目前支持的AppModel有4种(ASP.NET Core、Windows Forms、WPF和UWP),其中ASP.NET Core提供了全平台的支持,而Windows Forms、WPF和UWP只能在 Windows 上运行。CoreFX 是经过完全重写的 BCL,除自身具有跨平台执行的能力外,其提供的 API 也不再统一定义在少数几个单一的程序集中,而是经过有效划分之后被定义在各自独立的模块中。这些模块对应一个单一的程序集,并最终以 NuGet 包的形式进行分发。至于底层的虚拟机,微软则为主流的操作系统类型(Windows、macOS 和 Linux)和处理器架构(x86、x64和ARM)设计了针对性的运行时,被称为CoreCLR。
图2-21.NET Core层次结构(一)
作为运行时的CoreCLR和提供BCL的CoreFX是.NET Core重要的基石,但是就开发成本来看,微软在后者投入的精力是前者无法比拟的。我们知道.NET Core 自诞生到现在已经有很多年,目前的版本还只是到了 3.0,在发布进度上稍显缓慢,其中一个主要的原因是重写CoreFX 提供的基础 API 确实是一件烦琐且耗时的工程,而且这项工程远未结束。为了对CoreFX提供的BCL有一个大致的了解,下面介绍常用的基础API在哪些命名空间定义。
● System.Collections:定义了常用的集合类型。
● System.Console:提供API完成基本的控制台操作。
● System.Data:提供用于访问数据库的API,相当于原来的ADO.NET。
● System.Diagnostics:提供基本的诊断、调试和追踪的API。
● System.DirectoryServices:提供基于AD(Active Directory)管理的API。
● System.Drawing:提供GDI相关的API。
● System.Globalization:提供API实现多语言及全球化支持。
● System.IO:提供与文件输入和输出相关的API。
● System.NET:提供与网络通信相关的API。
● System.Numerics:定义一些数值类型作为基元类型的补充,如 BigInteger、Complex 和Plane等。
● System.Reflection:提供API以实现与反射相关的操作。
● System.Runtime:提供与运行时相关的一些基础类型。
● System.Security:提供与数据签名和加密/解密相关的API。
● System.Text:提供针对字符串/文本编码与解码相关的API。
● System.Threading:提供用于管理线程的API。
● System.Xml:提供API用于操作XML结构的数据。
对于传统的.NET Framework 来说,承载 BCL 的 API 几乎都定义在 mscorlib.dll 程序集中,这些API并不是全部都转移到组成CoreFX的众多程序集中,那些与运行时(CoreCLR)具有紧密关系的底层 API被定义到 System.Private.CoreLib.dll程序集中,所以图 2-22反映了真正的.NET Core 层次结构。我们在编程过程中使用的基础数据类型基本上都定义在这个程序集中,目前这个程序集的尺寸已经超过了 10MB。由于该程序集提供的 API 与运行时关联较为紧密,较之 CoreFX 提供的 API,这些基础 API 具有较高的稳定性,所以它们是随着CoreCLR一起发布的。
图2-22.NET Core层次结构(二)
虽然我们在编程过程中使用的绝大部分基础类型都定义在 System.Private.CoreLib.dll 程序集中,但是这是一个“私有”的程序集,从其命名可以看出这一点。将 System.Private.CoreLib.dll称为一个私有程序集,并不是因为定义其中的都是一些私有类型,而是因为在编程过程中不会真正引用这个程序集,这与.NET Framework 下的 mscorlib.dll 程序集是一样的。不仅如此,当.NET Core 代码被编译时,编译器也不会链接到这个私有程序集上,也就是说,编译后生成的程序集中也没有针对该程序集引用的元数据。但是当应用被真正执行的时候,所有引用的基础类型会全部自动“转移”到这个程序集中。而实现运行过程中的类型转移其实就是利用了 Type Forwarding技术。
实例演示:针对System.Private.CoreLib.dll程序集的类型转移
对于上面介绍的针对 System.Private.CoreLib.dll程序集的类型转移,很多读者可能还是难以理解,为了彻底认识这个问题,下面做一个简单的实例演示。首先利用 Visual Studio 创建一个.NET Core控制台应用,然后在作为程序入口的Main方法中编写如下几行代码,从而将常用的几个数据类型(System.String、System.Int32 和 System.Boolean)所在的程序集名称打印在控制台上。
根据上面的分析可知,程序运行过程中使用的这些基础类型全部来源于 System.Private.CoreLib.dll程序集,这在图2-23所示的输出结果中得到了证实。通过图2-23所示的输出结果,我们不仅可以知道这个核心程序集的名称,还可以知道该程序集目前的版本(4.0.0.0)。
图2-23 基础类型来源于System.Private.CoreLib.dll程序集
应用程序编译后生成的程序集并不会具有针对 System.Private.CoreLib.dll程序集引用的元数据,为了证明这一点,只需要利用 Windows SDK(在目录“%ProgramFiles(x86)%\Microsoft SDKs\Windows\{version}\Bin”下)提供的反编译工具 ildasm.exe 即可。利用 ildasm.exe 打开这个控制台应用编译生成的程序集之后,可以发现它具有如下两个程序集的应用。
实际上,程序只涉及 4个类型,即 1个 Console类型和 3个基础数据类型(String、Int32和Boolean),而程序集层面只有针对System.Runtime程序集和System.Console程序集的引用。毫无疑问,后面这 3个基础数据类型肯定与 System.Runtime程序集有关,要了解该程序集针对这 3个基础数据类型的相关定义,需要先知道这个程序集究竟被保存在哪里。“%ProgramFiles%\dotnet\”是.NET Core 应用的根目录,而 System.Runtime.dll 作为共享程序集被保存在子目录“\shared\Microsoft.NETCore.App\3.0.0”下面,这个目录下面还保存着很多其他的共享程序集。
我们依然利用反编译工具 ildasm.exe查看 System.Runtime.dll程序集清单文件的元数据定义。可以发现,整个程序集除了定义少数几个核心类型(如两个重要的委托类型Action和Func就定义在这个程序集中),它的作用就是将所有基础类型采用 Type Forwarding 方式转移到System.Private.CoreLib.dll 程序集中,下面的代码片段展示了针对程序使用的 3 个基础数据类型转移的相关定义。
演示实例体现的程序集直接的引用关系,以及上述代码片段体现的相关基础类型(System.String、System.Int32和 System.Boolean)的转移方向基本上体现在图2-24所示的关系图中。
图2-24 程序集引用关系和类型转移方向
复用.NET Framework程序集
上述利用Type Forwarding方式实现跨程序集类型转移的技术称为“垫片”(Shim),这是实现程序集跨平台复用的重要手段。除了 System.Runtime.dll 程序集,.NET Core 还提供了一些其他垫片程序集,正是源于这些垫片程序集的存在,我们可以将在.NET Framework环境下编译的程序集在.NET Core应用中使用。为了使读者对此有深刻认识,下面做一个简单的实例演示。
首先利用Visual Studio创建一个空的解决方案,然后添加图2-25所示的3个项目(NetApp、NetCoreApp、NetLib)。其中,NetApp 和 NetCoreApp 分别是针对.NET Framework(4.7)和.NET Core(3.0)的控制台程序,而NetLib则是针对.NET Framework的类库项目,该项目定义的API将在NetApp和NetCoreApp中被调用。
图2-25 在.NET Core应用中使用.NET Framework程序集
我们在 NetLib项目中定义了一个 Utils工具类,并在其中定义了一个 PrintAssemblyNames方法。如下面的代码片段所示,在这个方法中打印出 3 个常用的类型(Task、Uri 和 XmlWriter)所在的程序集的名称。通过在不同类型(.NET Framework和.NET Core)的应用程序中调用这个方法,我们可以确定它们在运行时究竟是从哪个程序集中加载的。另外,我们分别在NetApp和NetCoreApp这两个不同类型的控制台程序中调用了这个方法。
NetLib:
NetApp:
NetCoreApp:
直接运行NetApp和NetCoreApp这两个控制台程序后,会输出不同的结果。如图2-26所示,对于指定的3个类型(Task、Uri和XmlWriter),分别在.NET Framework和.NET Core环境下承载它们的程序集是不同的。具体来说,.NET Framework环境下的这3个类型分别定义在mscorlib.dll、System.dll 和 System.Xml.dll 中。当切换到.NET Core 环境后,运行时则会在 3 个私有的程序集System.Private.CoreLib.dll、System.Private.Uri.dll和System.Private.Xml.dll中加载这3个类型。
图2-26 同一个类型来源于不同的程序集
由于NetApp和NetCoreApp这两个控制台应用使用的是同一个针对.NET Framework编译的程序集 NetLib.dll,所以先利用反编译工具 ildasm.exe查看它具有怎样的程序集引用。如下面的代码片段所示,程序集NetLib.dll引用的程序集与控制台应用NetApp的输出结果是一致的。
之后,核心问题就变成Task、Uri和XmlWriter这3个类型在.NET Core运行环境下是如何转移到其他程序集中的。要回答这个问题,只需要利用 ildasm.exe 查看 mscorlib.dll、System.dll 和System.Xml.dll反编译这 3个程序集即可。这 3个程序集同样存在于“%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\3.0.0”目录下,通过反编译与它们相关的程序集就可以得到如下所示的相关元数据。
mscorlib.dll:
System.dll:
System.Xml.dll:
由上述代码片段可知,针对Task、Uri和XmlWriter这3个类型的转移共涉及6个程序集,从程序集(NetLib.dll)引用元数据角度来看,这3个类型分别定义在mscorlib.dll、System.dll和System.Xml.dll中。当程序运行的时候,这3个类型分别转移到3个私有程序集(System.Private.CoreLib、System.Private.Uri、System.Private.Xml)中。
2.3.2 统一的BCL
虽然.NET Core借助CoreCLR和CoreFX实现了真正的跨平台,但是目前的.NET Core仅仅提供 ASP.NET Core、UWP、WPF 和 Windows Forms 这 4 种编程模型,后面 3 种依旧专注于Windows平台。对于传统.NET Framework下面向桌面应用的WPF、Windows Forms和ASP.NET,依然被保留下来作为.NET的一大分支。除了.NET Framework和.NET Core,.NET还具有另一个重要分支,即Xamarin,它可以为iOS、OS和Android编写统一的应用(见图2-27)。
图2-27 由.NET Framework、.NET Core和Xamarin组成的全新.NET平台
虽然被微软重新布局的.NET平台只包含3个分支,但是之前遇到的代码复用问题依然存在。之前解决程序集服务的方案就是 PCL,但这并不是一种理想的解决方案,由于各个目标框架具有各种独立的 BCL,所以我们创建的 PCL项目只能建立在指定的几种兼容目标框架的 BCL交集之上。对于全新的.NET平台来说,这个问题可以通过提供统一的 BCL得到根本解决,这个统一的BCL被称为.NET Standard。
可以将.NET Standard称为新一代的PCL,PCL提供的可移植能力仅仅限于创建时就确定下来的几种目标平台,但是.NET Standard做得更加彻底,因为它在设计的时候就已经考虑针对三大分支的复用。如图2-28所示,.NET Standard为.NET Framework、.NET Core和Xamarin提供了统一的API,所以在这组标准API基础上编写的代码能被所有类型的.NET应用复用。
图2-28 为整个.NET提供统一API的.NET Standard
.NET Standard提供的 API主要是根据现有.NET Framework定义的,它的版本升级反映了其提供的 API 不断丰富的过程。Visual Studio 提供的相应的项目模板可以帮助我们创建基于.NET Standard的类库项目,这样的项目会采用专门的目标框架别名 netstandard{version}。一个针对.NET Standard 2.1 的类库项目具有如下定义,同时可以看到它采用的目标框架别名为.NET Standard 2.1。
顾名思义,.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的实现,只需要采用某种技术链接到现有的程序集即可。
一个针对.NET Standard编译生成的程序集在不同的执行环境中针对真正提供实现的程序集的所谓链接依然是通过垫片技术实现的。为了彻底搞清楚这个问题,下面做一个简单的实例演示。如图 2-29 所示,我们创建了与上面演示实例具有类似结构的解决方案,与之不同的是,分别针对.NET Framework 和.NET Core 的控制台应用 NetApp 与 NetCoreApp 共同引用的类库NetStandardLib是一个.NET Standard 2.1类库项目。
图2-29 在.NET Framework和.NET Core应用复用的.NET Standard 2.0类库
与上面演示的实例一样,我们在 NetStandardLib中定义了一个 Utils类,并利用其静态方法PrintAssemblyNames 将两个数据类型(Dictionary<,>和 SortedDictionary<,>)所在的程序集名称输出到控制台上,该方法分别在NetApp和NetCoreApp的入口Main方法中被调用。
NetStandardLib:
NetApp:
NetCoreApp:
直接运行这两个分别针对.NET Framework 和.NET Core 的控制台应用 NetApp 与NetCoreApp,可以发现它们会生成不同的输出结果。如图 2-30 所示,在.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。
图2-30 同一个类型来源于不同的程序集
对于演示的这个实例来说,NetStandardLib类库项目针对的目标框架为.NET Standard 2.1,后者最终体现为一个名为 NetStandard.Library的 NuGet包,这一点其实可以从 Visual Studio针对该项目的依赖节点看出来(见图2-31)。这个名为NetStandard.Library的NuGet包具有一个核心程序集netstandard.dll,上面的.NET Standard API就定义在该程序集中。
图2-31.NET Standard项目对NuGet包NetStandard.Library的依赖
也就是说,所有.NET Standard 2.1项目都具有针对程序集netstandard.dll的依赖,这个依赖也会体现在编译后生成的程序集上。对于演示实例中 NetStandardLib 类库项目编译生成的同名程序集来说,它对netstandard.dll程序集的依赖体现在如下所示的元数据中。
按照我们既有的知识,原本定义在 netstandard.dll 程序集的两个类型(Dictionary<,>和SortedDictionary<,>)在不同的执行环境中需要被转移到另一个程序集中,所以完全可以在相应的环境中提供一个同名的垫片程序集,并借助类型的跨程序集转移机制来实现,实际上微软也就是这么做的。下面先介绍针对.NET Framework的垫片程序集netstandard.dll的相关定义,我们可以直接在NetApp编译的目标目录中找到这个程序集。借助反编译工具ildasm.exe,可以得到与 Dictionary<,>和 SortedDictionary<,>这两个泛型字典类型转移相关的元数据,具体的代码片段如下。
针对.NET Core 的垫片程序集 netstandard.dll 被保存在共享目录“%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\2.0.0”下,我们采用同样的方式提取与 Dictionary<,>和SortedDictionary<,>这两个泛型字典类型转移相关的元数据。从下面的代码片段可以看出,Dictionary<,>和SortedDictionary<,>这两个类型都被转移到程序集System.Collections.dll之中。
由演示实例的执行结果可知,SortedDictionary<,>确实定义在程序集 System.Collections.dll 中,但是 Dictionary<,>类型则出自核心程序集 System.Private.CoreLib.dll,所以可以断定 Dictionary<,>类型在 System.Collections.dll 中必然出现了二次转移。为了确认该推论,我们只需要采用相同的方式反编译程序集 System.Collections.dll,该程序集也被存储在共享目录“%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\3.0.0”下,该程序集中针对 Dictionary<,>类型的转移体现在如下所示的元数据中。
Dictionary<,>和SortedDictionary<,>这两个类型在.NET Framework 4.7.2与.NET Core 3.0环境下的跨程序集转移路径基本上体现在图 2-32 中。简单来说,.NET Framework 环境下的垫片程序集netstandard.dll将这两个类型分别转移到程序集mscorlib.dll和System.dll之中。如果执行环境切换到.NET Core,这两个类型先被转移到程序集 System.Collection.dll 之中,但是Dictionary<,>类型最终是由 System.Private.CoreLib.dll 这个基础程序集承载的,所以在程序集System.Collection.dll中针对该类型做了二次转移。
图2-32 Dictionary<,>和SortedDictionary<,>分别针对.NET Framework和.NET Core的类型转移
上面这个简单的类型基本上揭示了.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 进行编译的,所以在各自环境中提供这个同名的程序集完成类型的转移即可。
2.3.3 展望未来
天下大势,分久必合,合久必分,技术发展亦是如此。当我们设计一个全新框架或者平台的时候,总是希望它尽可能地兼容和适配未来可能发生的变化,但是没有人能够准确地预测未来。如果现有的框架平台不能满足新需求,我们就需要在现有基础上开启另一个分支。
“开枝散叶”最初可能是权宜之计,但是随着时间的累积,我们发现这样的操作已经成为一种常见的解决方案,2.1 节描述的就是这种情况。“枝繁叶茂”其实是一种病态的繁荣,当它成为一种无法承受的负累的时候,对现有分支的整合就成为唯一的解决方案。
.NET正走在“大一统”的道路上,.NET Core只是这条漫长道路上的一个里程碑。2019年11月,微软推出.NET Core 3.1 LTS (Long Term Support),这将是最后一个版本的.NET Core。今后不会再有.NET Core和.NET Framework之分,未来的.NET就是图2-33所示的统一平台,我们将其称为.NET 5。由于所有类型的应用都是在统一的.NET Standard上开发的,所以不会再有移植性问题。按照微软提供的时间表,.NET 5 GA(General Available)将于2020年11月发布,而.NET 6 LTS(Long Term Support)的发布会发生在2021年以后。在正常情况下,一年一个大版本将是.NET未来的发展节奏,偶数年份和奇数年份发布的分别是 GA版本与 LTS 版本,或者奇数年份发布GA版本,偶数年份发布LTS版本。
图2-33 “大一统”的.NET 5