.NET Core跨平台的奥秘[中篇]:复用之殇

发表于:2017-11-21 09:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:51Testing软件测试网采编

  类型的转移
  在进行框架或者产品升级过程,我们经常会遇到针对程序集的合并和拆分的场景,比如在新版本中需要对现有的API进行从新规划,可能会将定义在程序集A中定义的类型转移到程序集B中。但是即使发生了这样的情况,我们依然需要为新框架或者产品提供向后兼容的能力,这就需要使用到所谓“类型转移(Type Forwarding)”的特性。
  为了让读者朋友们对类型转移这个重要的特性具有一个大体的认识,我们来作一个简单的实例演示。我们利用Visual Studio创建一个针对.NET Framework 3.5的控制台应用App,并在作为程序入口的Main方法中编写了如下两行代码将两个常用的类型(String和Func<>)所在的程序集名打印出来。程序编译之后会在 “\bin\Debug” 目录下生成可执行文件App.exe和对应的配置文件App.exe.config。从如下给出的配置文件内容可以看出.NET Framework 3.5采用的运行时(CLR)版本为 “v2.0.50727” 。
  1: class Program
  2: {
  3: static void Main()
  4: {
  5: Console.WriteLine (typeof(string).Assembly.FullName);
  6: Console.WriteLine (typeof(Func<>).Assembly.FullName);
  7: }
  8: }
  App.exe.config
  1: <configuration>
  2: <startup>
  3: <supportedRuntime&nbsp; version="v2.0.50727"/></startup>
  4: </startup>
  5: </configuration>
  现在我们直接以命令行的执行执行编译生成的App.exe后会在控制台上得到如下图所示的输出结果。可以看出对于我们给出的这两个基础类型(String和Func<>),只有String类型被定义在程序集mscorlib.dll之中,而类型Func<>其实被定义在另一个叫做System.Core.dll的程序集之中。其实Framework 2.0、3.0和3.5不仅仅共享相同的运行时(CLR 2.0),对于提供基础类型的核心程序集mscorlib.dll也是共享的,下图输出的版本信息已经说明了这一点。也就是说,.NET Framework 2.0发布时提供的程序集mscorlib.dll在.NET Framework 3.x时代就没有升级过。Func<>类型是在.NET Framework 3.5发布时提供的一个基础类型,所以不得不将它定义在一个另一个程序集中,微软将这个程序集命令为System.Core.dll。
  现在我们看看.NET Framework 4.0(CLR 4.0)环境下运行同一个应用程序(App.exe)是否会有不同的输出结果。为此我们在不对项目做重新编译情况下直接修改配置文件App.exe.config,并按照如下所示的方式将运行时版本设置为4.0。
  1: <configuration>
  2: <startup>
  3: <supportedRuntime&nbsp;version="v4.0"/>
  4: </startup>
  5: </configuration>
  下图是同一个App.exe在.NET Framework 4.0环境下的输出结果,可以看出我们提供的两个基础类型所在的程序集都是mscorlib.dll。也就是当.NET Framework升级到4.0之后,不仅仅运行时升级到了全新的CLR 4.0,微软同时也对承载基础类型的mscorelib.dll程序集进行了重新规划,所以定义在System.Core.dll程序集中的基础类型也基本上又重新回到了mscorlib.dll这个本应该属于它的程序集中。
  我们来继续分析上面演示的这个程序。由于App.exe这个程序集最初是针对目标框架.NET Framework 3.5编译生成的,所以它的清单文件将包含针对mscorlib.dll(2.0.0.0)和System.Core.dll(3.5.0.0)的程序集引用。下面的代码片段展示了针对这两个程序集引用的元数据的定义。
  1: .assembly extern mscorlib
  2: {
  3: .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) 
  4: .ver 2:0:0:0
  5: }
  6: .assembly extern System.Core
  7: {
  8: .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) 
  9: .ver 3:5:0:0
  10: }
  当App.exe在.NET Framework 4.0环境中运行时,由于它的元数据提供的是针对System.Core.dll程序集的引用,所以CLR总是试图加载该程序集并从中定位目标类型(比如我们演示实例中的类型Func<>)。如果当前运行环境无法提供这个程序集,那么毫无疑问,一个FileNotFoundException类型的异常会被抛出来。也就是,虽然类型Func<>在.NET Framework 4.0中已经转移到了新的程序集mscorlib.dll中,当前环境依然会提供一个文件名为System.Core.dll的程序集。
  System.Core.dll存在的目的是告诉CLR它需要加载的类型已经发生转移,并将该类型所在的新的程序集名称告诉它,那么.NET Framework 4.0环境中的System.Core.dll是如何描述类型Func<>已经转移到程序集mscorelib.dll之中了呢?如果分析程序集System.Core.dll中的元数据,我们可以看到如下一段于此相关的代码。在程序集的清单文件中,每一个被转移的类型都对应这个这么一个 “.class extern forwarder” 指令。
  1: .class extern forwarder System.Func`1
  2: {
  3: .assembly extern mscorlib
  4: }
  不同于上面介绍的Retargetable程序集,类型的转移并不是只针对.NET Framework提供的基础程序集,如果我们自己开发的项目也需要提供类似的向后兼容性,也可以使用这个特性。针对类型转移类型的编程只涉及到一个类型为TypeForwardedToAttribute的特性,接下来我们通过一个简单的实例来演示一下如何利用这个特性将某个类型转移到一个新的程序集中。
  我们利用Visual Studio创建了如下图所示的解决方案,它演示了这样一个场景:控制台应用使用到了V1版本的类库Lib(v1\Lib),其中涉及到一个核心类型Foobar。该类库升级到V2版本时,我们选择将所有的核心类型统一定义在新的程序集Lib.Core中,所以类型Foobar需要转移到Lib.Core中。作为类库的发布者,我们希望使用到V1版本的应用能够直接升级到V2版本,也就是升级的应用不需要在引用新的Lib.Core程序集情况下对源代码进行重新编译,而是直接部署V2版本的两个程序集(Lib.dll和Lib.Core)就可以了。
  上图中的虚线箭头和实线箭头分别代表项目之间的引用关系,我们从中可以看出v2目录下的Lib项目具有对Lib.Core项目的引用,因为它需要引用转移到Lib.Core项目中的类型。为了完成针对类型Foobar的转移,我们只需要在v2\Lib中定义如下一行简单的代码就可以了,我们将这行代码定义在AssemblyInfo.cs文件中。
  [assembly: System.Runtime. CompilerServices.TypeForwardedTo (typeof(Lib.Foobar))]
  为了检验针对Foobar类型的转移是否成功,我们在控制台应用App中定义了如下一段程序,它负责将Foobar类型当前所在程序集的名称输出到控制台上。接下来我们只需要编译(以Debug模式)整个解决方案,那么V2版本的两个程序集(Lib.dll和Lib.Core.dll)将保存到\v2\lib\bin\debug\目录下。
  1: class Program
  2: {
  3: static void Main()
  4: {
  5: Console.WriteLine(typeof(Foobar).Assembly.FullName); 
  6: }
  7: }
  接下来我们采用命令行的形式来运行控制台程序App.exe。如下图所示,我们将当前目录切换到App.exe所在的目录(\app\bin\debug)下并执行App.exe,输出的结果表明Foobar类型当前所在的程序集为Lib.dll。接下来我们将针对V2版本的两个程序集拷贝进来后再次执行App.exe,我们发现此时的Foobar类型已经是从新的程序集Lib.Core.dll中加载的了。
  我们顺便来查看一下V2版本程序集Lib.dll的清单文件的内容。如下面的代码片段所示,在源代码中通过使用TypeForwardedToAttribute特性定义的类型转移在编译之后被转换成了一个“.class extern forwarder”指令。
  1: .assembly extern Lib.Core
  2: {
  3: .ver 1:0:0:0
  4: }
  5: .class extern forwarder Lib.Foobar
  6: {
  7: .assembly extern Lib.Core
  8: }
  9: …
  三、可移植类库(PCL)
  在.NET Framework的时代,创建可移植类库(PCL:Portable Class Library)是实现跨多个目标框架程序集共享的唯一途径。上面介绍的内容都是在为PCL做铺垫,只有充分理解了Retargetable程序集和类型转移的前提下才可能了解PCL的实现原理有正确的理解。考虑到很多读者朋友并没有使用PCL的经历,所以我们先来介绍一下如何创建一个PCL项目。 当我们采用Visualization Studio的Class Library(Portal)项目模板创建一个PCL项目的时候,需要在如下图所示的对话框中选择支持的目标框架及其版本。Visual Studio会为新建的项目添加一个名为 “.NET” 的引用,这个引用指向一个由选定目标框架决定的程序集列表。由于这些程序集提供的API能够兼容所有选择的平台,我们在此基础编写的程序自然也具有平台兼容性。
  如果查看这个特殊的.NET引用所在的地址,我们会发现它指向目录“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable\{version}\Profile\ProfileX”。如果查看 “%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable” 目录,我们会发现它具有如下图所示的结构。
  如上图所示,本机所在目录“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable”下具有三个代表.NET Framework版本的子目录(v4.0、v4.5和v4.6)。具体到针对某个.NET Framework版本的目录(比如v4.6),其子目录Profile下具有一系列以 “Profile” + “数字” (比如Profile31、Profile32和Profile44等)命名的子目录,实际上PCL项目引用的就是存储在这些目录下的程序集。
  对于两个不同平台的.NET Framework来说,它们的BCL在API的定义上存在交集,从理论上来说,建立在这个交集基础上的程序是可以被这两个平台中共享的。如下图所示,如果我们编写的代码需要分别对Windows Desktop/Phone、Windows Phone/Store和Windows Store/Desktop平台提供支持,那么这样的代码依赖的部分仅限于两两的交集A+B、A+C和A+D。如果要求这部分代码能够运行在Windows Desktop/Phone/Store三个平台上,那么它们只能建立在三者之间的交集A上。
  针对所有可能的目标框架(包括版本)的组合,微软会将作为两者交集的API提取出来并定义在相应的程序集中。比如说所有的目标框架都包含一个核心的程序集mscorlib.dll,虽然定义其中的类型及其成员在各个目标框架不尽相同,但是它们之间肯定存在交集,微软针对不同的目标框架组合将这些交集提取出来并定义在一系列同名程序集中,并同样命名为mscorlib.dll。 微软按照这样的方式创建了其他针对不同.NET Framework平台组合的基础程序集,这些针对某个组合的所有程序集构成一系列的Profile,并定义在上面我们提到过的目录下。值得一提的是,所有这些针对某个Profile的程序集均为Retargetable程序集。
  当我们创建一个PCL项目的时候,第一个必需的步骤是选择兼容的目标框架(和版本),Visual Studio会根据我们的选择确定一个具体的Profile,并为创建的项目添加针对该Profile的程序集引用。由于所有引用的程序集是根据我们选择的目标框架组合 “度身定制” 的,所以定义在PCL项目的代码才具有可移植的能力。
  上面我们仅仅从开发的角度解释了定义在PCL项目的代码本身为什么能够确保是与目标.NET Framework平台兼容的,但是在运行的角度来看这个问题,却存在额外两个问题:
  元数据描述的引用程序集与真实加载的程序集不一致,比如我们创建一个兼容.NET Framework 4.5和Silverlight 5.0的PCL项目,被引用的程序集mscorlib.dll的版本为2.0.5.0,但是Silverlight 5.0运行时环境中的程序集mscorlib.dll的版本则为5.0.5.0。
  元数据描述的引用程序集的类型定义与运行时加载程序集类型定义不一致,比如引用程序集中的某个类型被转移到了另一个程序集中。
  由于PCL项目在编译时引用的均为Retargetable程序集,所以程序集的重定向机制帮助我们解决了第一个问题。因为在CLR在加载某个Retargetable程序集的时候,如果找不到一个与引用程序集在文件名、版本、语言文化和公钥令牌完全匹配的程序集,则会只考虑文件名的一致性。至于第二个问题,自然可以通过上面我们介绍的类型转移机制来解决。
  综上所述,虽然微软在针对多个目标框架的代码复用上面为我们提供了一些解决方案。在源代码共享方面,我们可以采用共享项目,虽然共享项目能够做到将一组源文件进行打包复用,但是我个人基本上不怎么用它,因为如果我们在其中定义一些公有类型,那么引用该共享项目的项目之间会造成命名冲突。从另一方面讲,我们真正需要的是程序集层面的复用,但是在这方面微软只为我们提供了PCL。PCL这种采用提取目标框架API交集的方式注定了只能是一种临时的解决方案,试着想一下:如果目标框架由10种,每种有3个版本,我们需要为多少种组合创建相应的Profile。对于开发者来说,如果目标框架(包括版本),我们在创建PCL项目进行兼容框架的选择都会成问题。所以我们针对希望的是能够提供给全平台支持的BCL,你可以已经知道了,这就是Net Standard,那么Net Standard是如何能够在多个目标框架中复用的呢?请求关注本系列终结篇《.NET Core跨平台的奥秘[下篇]:全新的布局》。  
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号