在默认情况下,如果某个程序集引用了另一个具有强签名的程序集,CLR在执行的时候总是会根据程序集文件名、版本和公钥令牌去定位目标程序集。如果无法找到一个与之完全匹配的程序集,一般情况下会抛出一个FileNotFoundException类型的异常。如果当前引用的是一个Retargetable程序集,则意味着CLR在定位目标程序集的时候可以 “放宽” 匹配的要求,即指要求目标程序集具有相同的文件名即可。

如下图所示,我们的应用程序(App)引用了具有强签名的程序集“Foobar, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”,所以对于编译后生成的程序集App.exe来说,对应的程序集引用将包含目标程序集的文件名、版本和公钥令牌。如果在运行的时候只提供了一个有效名称为“Foobar, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d7fg7asdf7asd7aer”的程序集,除了文件名,后者的版本号和公钥令牌都与程序集引用元数据描述的都不一样。在默认情况下,系统此时总是会抛出一个FileNotFoundException类型的异常,倘若Foobar是一个Retargetable程序集,我们提供的将作为目标程序集被加载并使用。

2-14_thumb[2]

除了定义程序集的元数据多了如下一个retargetable标记之外,Retargetable程序集与普通程序集并没有本质区别。

普通程序集:  

<span style="color:#000000"><span style="background-color:#ffffff">.assembly Foobar</span></span>

Retargetable程序集: 

<span style="color:#000000"><span style="background-color:#ffffff">.assembly <span style="color:#ff0000">retargetable</span> Foobar</span></span>

这样一个retargetable标记可以通过按照如下所示的方式在程序集上应用AssemblyFlagsAttribute特性来添加。不过这样的重定向仅仅是针对.NET Framework自身提供的基础程序集有效,虽然我们也可以通过使用AssemblyFlagsAttribute特性为自定义的程序集添加这样一个retargetable标记,但是CLR并不会赋予它重定向的能力。

<span style="color:#000000"><span style="background-color:#ffffff">[assembly:AssemblyFlags(AssemblyNameFlags.Retargetable)] </span></span>

如果某个程序集引用了一个Retargetable程序集,自身清单文件针对该程序集的引用元数据同样具有如下所示的retargetable标记。CLR正式利用这个标记确定它引用的是否是一个Retargetable程序集,进而确定针对该程序集的加载策略,即采用针对文件名、版本和公钥令牌的完全匹配策略,还是采用只针对文件名的降级匹配策略。

针对普通程序集的引用:

<span style="color:#000000"><span style="background-color:#ffffff">   1: 针对普通程序集的引用</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: .assembly extern Foobar</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         </span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5:   .ver 1:0:0:0</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   6: }</span></span>

针对Retargetable程序集的引用:

<span style="color:#000000"><span style="background-color:#ffffff">   1: .assembly extern <span style="color:#ff0000">retargetable</span> Foobar</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89)                         </span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:   .ver 1:0:0:0</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5: }</span></span>

类型的转移

在进行框架或者产品升级过程,我们经常会遇到针对程序集的合并和拆分的场景,比如在新版本中需要对现有的API进行从新规划,可能会将定义在程序集A中定义的类型转移到程序集B中。但是即使发生了这样的情况,我们依然需要为新框架或者产品提供向后兼容的能力,这就需要使用到所谓“类型转移(Type Forwarding)”的特性。

为了让读者朋友们对类型转移这个重要的特性具有一个大体的认识,我们来作一个简单的实例演示。我们利用Visual Studio创建一个针对.NET Framework 3.5的控制台应用App,并在作为程序入口的Main方法中编写了如下两行代码将两个常用的类型(StringFunc<>)所在的程序集名打印出来。程序编译之后会在 “\bin\Debug” 目录下生成可执行文件App.exe和对应的配置文件App.exe.config。从如下给出的配置文件内容可以看出.NET Framework 3.5采用的运行时(CLR)版本为 “v2.0.50727” 。

<span style="color:#000000"><span style="background-color:#ffffff">   1: class Program</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:     static void Main()</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:     {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5:         Console.WriteLine(typeof(string).Assembly.FullName);</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   6:         Console.WriteLine(typeof(Func<>).Assembly.FullName);</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   7:     }</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   8: }</span></span>

App.exe.config

<span style="color:#000000"><span style="background-color:#ffffff">   1: <configuration></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2:   <startup></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:     <supportedRuntime&nbsp;version="<span style="color:#ff0000">v2.0.50727</span>"/></startup></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:   </startup></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5: </configuration></span></span>

现在我们直接以命令行的执行执行编译生成的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

2-15_thumb2

 

现在我们看看.NET Framework 4.0(CLR 4.0)环境下运行同一个应用程序(App.exe)是否会有不同的输出结果。为此我们在不对项目做重新编译情况下直接修改配置文件App.exe.config,并按照如下所示的方式将运行时版本设置为4.0。

<span style="color:#000000"><span style="background-color:#ffffff">   1: <configuration></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2:   <startup></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:     <supportedRuntime&nbsp;version="<span style="color:#ff0000">v4.0</span>"/></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:   </startup></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5: </configuration></span></span>

下图是同一个App.exe在.NET Framework 4.0环境下的输出结果,可以看出我们提供的两个基础类型所在的程序集都是mscorlib.dll。也就是当.NET Framework升级到4.0之后,不仅仅运行时升级到了全新的CLR 4.0,微软同时也对承载基础类型的mscorelib.dll程序集进行了重新规划,所以定义在System.Core.dll程序集中的基础类型也基本上又重新回到了mscorlib.dll这个本应该属于它的程序集中。

2-16_thumb2

我们来继续分析上面演示的这个程序。由于App.exe这个程序集最初是针对目标框架.NET Framework 3.5编译生成的,所以它的清单文件将包含针对mscorlib.dll(2.0.0.0)和System.Core.dll(3.5.0.0)的程序集引用。下面的代码片段展示了针对这两个程序集引用的元数据的定义。

<span style="color:#000000"><span style="background-color:#ffffff">   1: .assembly extern mscorlib</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         </span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:   .ver <span style="color:#ff0000">2:0:0:0</span></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5: }</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   6: .assembly extern System.Core</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   7: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   8:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         </span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   9:   .ver <span style="color:#ff0000">3:5:0:0</span></span></span>
<span style="color:#000000"><span style="background-color:#ffffff">  10: }</span></span>

当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” 指令。

<span style="color:#000000"><span style="background-color:#ffffff">   1: .class extern <span style="color:#ff0000">forwarder</span> System.Func`1</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:   .assembly extern mscorlib</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4: }</span></span>

不同于上面介绍的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)就可以了。

2-17_thumb2

上图中的虚线箭头和实线箭头分别代表项目之间的引用关系,我们从中可以看出v2目录下的Lib项目具有对Lib.Core项目的引用,因为它需要引用转移到Lib.Core项目中的类型。为了完成针对类型Foobar的转移,我们只需要在v2\Lib中定义如下一行简单的代码就可以了,我们将这行代码定义在AssemblyInfo.cs文件中。

<span style="color:#000000"><span style="background-color:#ffffff">   [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Lib.Foobar))] </span></span>

为了检验针对Foobar类型的转移是否成功,我们在控制台应用App中定义了如下一段程序,它负责将Foobar类型当前所在程序集的名称输出到控制台上。接下来我们只需要编译(以Debug模式)整个解决方案,那么V2版本的两个程序集(Lib.dll和Lib.Core.dll)将保存到\v2\lib\bin\debug\目录下。

<span style="color:#000000"><span style="background-color:#ffffff">   1: class Program</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:     static void Main()</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4:     {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5:         Console.WriteLine(typeof(Foobar).Assembly.FullName); </span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   6:     }</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   7: }</span></span>

接下来我们采用命令行的形式来运行控制台程序App.exe。如下图所示,我们将当前目录切换到App.exe所在的目录(\app\bin\debug)下并执行App.exe,输出的结果表明Foobar类型当前所在的程序集为Lib.dll。接下来我们将针对V2版本的两个程序集拷贝进来后再次执行App.exe,我们发现此时的Foobar类型已经是从新的程序集Lib.Core.dll中加载的了。

2-18_thumb2

我们顺便来查看一下V2版本程序集Lib.dll的清单文件的内容。如下面的代码片段所示,在源代码中通过使用TypeForwardedToAttribute特性定义的类型转移在编译之后被转换成了一个“.class extern forwarder”指令。

<span style="color:#000000"><span style="background-color:#ffffff">   1: .assembly extern Lib.Core</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   2: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   3:   .ver 1:0:0:0</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   4: }</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   5: .class extern <span style="color:#ff0000">forwarder</span> Lib.Foobar</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   6: {</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   7:   .assembly extern Lib.Core</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   8: }</span></span>
<span style="color:#000000"><span style="background-color:#ffffff">   9: …</span></span>

三、可移植类库(PCL)

在.NET Framework的时代,创建可移植类库(PCL:Portable Class Library)是实现跨多个目标框架程序集共享的唯一途径。上面介绍的内容都是在为PCL做铺垫,只有充分理解了Retargetable程序集类型转移的前提下才可能了解PCL的实现原理有正确的理解。考虑到很多读者朋友并没有使用PCL的经历,所以我们先来介绍一下如何创建一个PCL项目。 当我们采用Visualization Studio的Class Library(Portal)项目模板创建一个PCL项目的时候,需要在如下图所示的对话框中选择支持的目标框架及其版本。Visual Studio会为新建的项目添加一个名为 “.NET” 的引用,这个引用指向一个由选定目标框架决定的程序集列表。由于这些程序集提供的API能够兼容所有选择的平台,我们在此基础编写的程序自然也具有平台兼容性。

2-20_thumb[2]

 

更多推荐