“前.NET Core时代”如何实现跨平台代码重用 ——程序集重用

除了在源代码层面实现共享(“前.NET Core时代”如何实现跨平台代码重用 ——源文件重用)之外,我们还可以跨平台共享同一个程序集,这种独立于具体平台的“中性”程序集通过创建一种名为“可移植类库(PCL:
Portable Class
Library)”项目来实现。为了让读者朋友们对PCL的实现机制具有充分的认识,我们先来讨论一个被我称为“程序集动态绑定”的话题。

目录
一、何谓程序集动态绑定?
二、程序集一致性
三、程序集重定向
四、类型的转移
五、可移植类库(PCL)

一、何谓程序集动态绑定?

我们采用C#、VB.NET这样的编程语言编写的源文件经过编译会生成有IL代码和元数据构成的托管模块,一个或者多个托管模块合并生成一个程序集。除了包含必要的托管模块之外,我们还可以将其他文件作为资源内嵌到程序集中,程序集的文件构成一个“清单(Manifest)”文件来描述,这个清单文件包含在某个托管模块中。

元数据使程序集成为一个自描述性(Self-Describling)的部署单元,除了描述定义在本程序集中所有类型之外,这些元数据还包括对引用自外部程序集的所有类新的描述。包含在元数据中针对外部程序集的描述是由编译时引用的程序集决定的[1],引用程序集的名称(包含文件名、版本、语言文化和签名的公钥令牌)会直接体现在当前程序集的元数据中。

在运行时,通过元数据描述的引用程序集信息是CLR定位目标程序集的依据,但是这并不意味着它与实际加载的程序集是完全一致的,后者实际上是根据当前执行环境动态加载的,我们姑且将这个机制成为“程序集动态绑定”。

二、程序集一致性

我们都知道.NET
Framework是向后兼容的,也就是说原来针对低版本.NET
Framework编译生成的程序集是可以直接在高版本CLR下运行的。我们试想一下这么一个问题:就一个针对.NET Framework
2.0编译生成的程序集自身来说,所有引用的.NET
Framework程序集的版本都是2.0,如果这个程序集在4.0环境下执行,CLR在决定加载它所依赖程序集的时候,应该选择2.0还是4.0呢?

我们不妨通过实验来获得这个问题的答案。我们利用Visual Studio创建一个针对.NET Framework 2.0的控制台应用(命名为App),并在作为程序入口的Main方法上编写如下一段代码。如下面代码片断所示,我们在控制台上输出了三个基本类型(Int32、XmlDocument和DataSet)所在程序集的全名。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine(typeof(int).Assembly.FullName);
   6:         Console.WriteLine(typeof(XmlDocument).Assembly.FullName);
   7:         Console.WriteLine(typeof(DataSet).Assembly.FullName);
   8:     }
   9: }

直接运行这段程序使之在默认版本的CLR(2.0)下运行会在控制台上输出如下的结果,我们会发现上述三个基本类型所在程序集的版本都是2.0.0.0。在这种情况下,运行时加载的程序集和编译时引用的程序集是一致的。

   1: mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   2: System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   3: System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

现在我们直接在目录“\bin\debug”直接找到以Debug模式编译生成的程序集App.exe,并为之创建一个配置文件(命名为App.exe.config)。我们编写了如下一段配置,其目的在于选择4.0版本的CLR运行这个程序。

   1: <configuration>
   2:   <startup>
   3:     <supportedRuntime version="v4.0"/>
   4:   </startup>
   5: </configuration>

或者:

   1: <configuration>
   2:   <startup>
   3:     <requiredRuntime version="v4.0"/>
   4:   </startup>
   5: </configuration>

在无需重新编译(确保运行的依然是针对.NET Framework 2.0编译生成的程序集)直接运行App.exe,我们会在控制台上得到如下所示的输出结果,可以看到三个程序集的版本编程了4.0.0.0。

   1: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   2: System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   3: System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

这个简单的实例体现了这么一个特征:运行过程中加载的.NET
Framework程序集(承载FCL的程序集)是由当前运行时(CLR)决定的,这些程序集的版本总是与CLR的版本相匹配。包含在元数据中的程序集信息提供目标程序集的名称,而版本则由当前运行的CLR来决定,我们将这个重要的机制称为“程序集一致性(Assembly
Unification)”,下图很清晰地揭示了这个特性。

三、程序集重定向

在默认情况下,如果某个程序集引用了另一个具有强签名的程序集,CLR在执行的时候总是会根据程序集有效名称(Assembly
Qualified
Name,由程序集文件名、版本、语言文化和公钥令牌组成)去定位目标程序集,如果无法找到一个与之完全匹配的程序集,一般情况下会抛出一个FileNotFoundException类型的异常。程序集的重定向机制实际上是让CLR在定位目标程序集的时候“放宽”了匹配的条件,即指要求目标程序集的文件名与元数据描述的程序集一致即可。

如下图所示,程序集(Lib.dll)在编译的时候引用了可被重定向的程序集“Retargetable, Version=1.0.0.0,
Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a”。在采用运行时Runtime1和Runtime2所在的执行环境下,真正绑定的目标程序集分别为“Retargetable,
Version=2.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35”和“Retargetable, Version=3.0.0.0,
Culture=neutral, PublicKeyToken
=30ad4fe6b2a6aeed”,除了程序集文件名称,它们的版本和公钥令牌与编译时引用的程序集均不相同。

实际上通过PCL项目编译生成的程序集所引用的都是这种能够被重定向的程序集(以下简称Retargetable程序集)。与普通程序集相比较,这种可被重定向的程序集的唯一不同之处在于它多了一个如下所示的retargetable标记。

   1: 普通程序集
   2: .assembly Lib
   3:  
   4: 可被重定向程序集
   5: .assembly  Lib

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

   1: [assembly:AssemblyFlags(AssemblyNameFlags.Retargetable)]

对于某个程序集来说,针对普通程序集的引用和Retargetable程序集的引用的不同支持会反映在自身的元数据中。下面的代码片断体现了元数据对引用程序集的描述,我们可以看到针对Retargetable程序集的引用同样具有一个retargetable标记。当CLR在定位目标程序集的时候就是根据这个标记决定是否需要重定向到当前运行时环境下与之匹配的程序集,并且这个程序集有可能在版本和公钥令牌均与元数据描述不同。

   1: 针对普通程序集的引用
   2: .assembly extern Lib
   3: {
   4:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   5:   .ver 1:0:0:0
   6: }
   7:  
   8: 针对Retargetable程序集的引用
   9: .assembly extern  Lib
  10: {
  11:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89)                         
  12:   .ver 1:0:0:0
  13: }

四、类型的转移

所谓类型转移(Type Forwarding)就是将定义在某个程序集中的类型转移到另一个程序集中。我们先通过一个简单的实例让读者朋友们对类型转移有一个感官上的认识。我们利用Visual
Studio创建一个针对.NET Framework
3.5的控制台应用,并编写如下一端简单的程序输出两个常用的类型(Function<T>和TimeZoneInfo)所在程序集的名称。现在我们直接运行这个程序,会在控制台上得到如下所示的输出结果,可以看出.NET
Framework 3.5(CLR 2.0)环境下的这两个类型定义在程序集System.Core.dll中。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine(typeof(Func<>).Assembly.FullName);
   6:         Console.WriteLine(typeof(TimeZoneInfo).Assembly.FullName);
   7:     }
   8: }

输出结果:

   1: , Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   2: , Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

现在我们对该程序的配置文件(App.config)作如下的修改,其目的在于采用CLR
4.0来运行该程序。再次运行该程序集之后,我们会在控制台上得到不一样的输出结果。通过如下所示的输出结果我们可以看出当.NET
Framework从3.5升级到4.0的时候,将原本定义在程序集System.Core.dll中的部分类型转移到了程序集mscorelib.dll之中。

   1: <configuration>
   2:   <startup>
   3:     <supportedRuntime version="v4.0"/>
   4:   </startup>
   5: </configuration>

输出结果:

   1: , Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 
   2: , Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

跨程序集之间的类型转移帮助框架或者类库的提供者解决这样的难题:某个类型在框架1.0版本的时候定义在程序集A中,当升级到2.0的时候被转移到了程序集B中,使用旧版本的应用可以在不做任何修改的情况下直接对使用的框架进行升级。类型转移需要使用到一个特殊的特性TypeForwardedToAttribute,我们现在通过一个简单的实例来演示如何利用这个特性来解决框架或者类库升级过程在类型跨程序集转移的问题。

这个演示的场景如上图所示:代表应用的App.exe在编译的时候引用了代表框架的程序集Lib.dll,具体使用的是定义其中的类型Foobar,框架进行升级之后新增了一个程序集Lib2.dll,原来定义在Lib.dll中的类型Foobar被转移到了Lib2.dll中。充分利用CLR针对类型转移的支持,我们只需要直接部署新版本的Lib.dll(不包含类型Foobar)和Lib2.dll,现有的程序能够照常运行。

我们利用Visual
Studio创建了如上图所示的解决方案。类库项目Lib1代表版本1.0的框架,我们将编译生成的程序集名称设置成Lib,并在其中定义了一个类型Foobar。控制台应用直接应用Lib1,并与其中编写了如下一段简单的程序,其目的在于确认类型Foobar所在的程序集。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Console.WriteLine(typeof(Foobar).AssemblyQualifiedName);
   6:         Console.Read();
   7:     }
   8: }

类库项目Lib2和Lib3编译生成代表框架升级之后的两个程序集,我们通过修改项目属性将目标程序集名称设置成Lib和Lib2,Lib2具有针对Lib3的项目引用。我们在Lib3中重新定义了代表被转移的类型Foobar,而Lib2实际上是一个空的项目。要体现类型Foobar从Lib.dll转移到Lib2.dll,我们需要在Lib2项目上应用如下所示的一个TypeForwardedToAttribute特性(定义在AssemblyInfo.cs中)。

   1: [assembly:TypeForwardedTo(typeof(Foobar))] 

现在我们对整个解决方案进行编译,然后定位到控制台App项目编译后的输出目录(app\bin\debug),并将项目Lib1编译生成的程序集Lib.dll删除,而将Lib2和Lib3编译生成的程序集Lib.dll和Lib2.dll拷贝到该目录下。现在我们直接运行App.exe,我们会在控制台上得到如下所示的输出结果。

   1: Lib.Foobar, , Version=2.0.0.0, Culture=neutral, PublicKeyToken=null 

如果某个项目应用了TypeForwardedToAttribute特性指向定义在另一个程序集中的被转出类型,类型转移相关的信息会体现在编译生成的元数据中。就我们的实例而言,项目Lib2编译的生成的程序集通过如下的元数据来指向被转移出去的类型所在的目标程序集。

   1: .class extern  Lib.Foobar
   2: {
   3:   .assembly extern Lib2
   4: }

当App.exe被执行的时候,由于元数据体现的依然是针对程序集Lib.dll的引用,所以CLR任然会试图从该程序集中加载类型Foobar。但是通过分析程序集Lib.dll的元数据,CLR知道Foobar已经被转移到程序集Lib2.dll中,所以定义在其中的同名类型Foobar最终会被加载。

五、可移植类库(PCL)

就目前来说,创建PCL项目是实现跨.NET Framework平台程序集共享唯一的方式。当我们采用Class
Library(Portal)项目模板创建一个PCL项目的时候,需要在如下图所示的对话框中选择支持的目标平台及其版本。Visual
Studio会为新建的项目添加一个名为“.NET”的引用,这个引用指向一个由选定.NET
Framework平台决定的程序集列表。由于这些程序集提供的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来说,它们的Core
Library在API的定义上存在交集,从理论上来说,建立在这个交集基础上的程序是可以被这两个平台中共享的。如下图所示,如果我们编写的代码需要分别对Windows
Desktop/Phone、Windows Phone/Store和Windows
Store/Desktop平台提供支持,那么这样的代码依赖的部分仅限于两两的交集A+B、A+C和A+D。如果要求这部分代码能够运行在Windows
Desktop/Phone/Store三个平台上,那么它们只能建立在三者之间的交集A上。

 

针对所有可能的.NET Framework平台(包括版本)的组合,微软会将体现在Core
Library上的交集提取出来并定义在相应的程序集中。比如说所有的.NET
Framework平台都包含一个核心的程序集mscorelib.dll,虽然定义其中的类型及其成员在各个.NET
Framework平台不尽相同,但是它们之间肯定存在交集,微软针对不同的.NET
Framework平台组合将这些交集提取出来并定义在一系列同名程序集中,并同样命名为mscorelib.dll。
微软按照这样的方式创建了其他针对不同.NET
Framework平台组合的基础程序集,这些针对某个组合的所有程序集构成一系列的Profile,并定义在上面我们提到过的目录下。值得一提的是,所有这些针对某个Profile的程序集均为Retargetable程序集。

当我们创建一个PCL项目的时候,第一个必需的步骤是选择兼容的.NET Framework平台,Visual
Studio会根据我们的选择确定一个具体的Profile,并为创建的项目添加针对该Profile的程序集引用。由于所有引用的程序集是根据我们选择的.NET
Framework平台“度身定制”的,所以定义在PCL项目的代码才具有可移植的能力。

上面我们仅仅从开发的角度解释了定义在PCL项目的代码本身为什么能够确保是与目标.NET Framework平台兼容的,但是在运行的角度来看这个问题,却存在额外两个问题:

  • 元数据描述的引用程序集与真实加载的程序集不一致,比如我们创建一个兼容.NET Framework 4.5和Silverlight
    5.0的PCL项目,被引用的程序集mscorellib.dll的版本为2.0.5.0,但是Silverlight
    5.0运行时环境中的程序集mscorellib.dll的版本则为5.0.5.0。
  • 元数据描述的引用程序集的类型定义与运行时加载程序集类型定义不一致,比如引用程序集中的某个类型被转移到了另一个程序集中。

由于PCL项目在编译时引用的均为Retargetable程序集,所以程序集的重定向机制帮助我们解决了第一个问题。因为在CLR在加载某个Retargetable程序集的时候,如果找不到一个与引用程序集在文件名、版本、语言文化和公钥令牌完全匹配的程序集,则会只考虑文件名的一致性。至于第二个问题,自然可以通过上面我们介绍的类型转移机制来解决。



[1] 当我们执行C#编译器(csc.exe)以命令行的形式编译C#源代码时,引用的程序集通过“/reference”开关指定。

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-10-29 01:30:20

“前.NET Core时代”如何实现跨平台代码重用 ——程序集重用的相关文章

“前.NET Core时代”如何实现跨平台代码重用 ——源文件重用

微软在2002年推出了第一个版本的 .NET Framework,这是一个主要面向Windows 桌面(Windows Forms)和服务器(ASP.NET Web Forms)的基础框架.在此之后,PC的霸主地位不断受到其他设备的挑战甚至取代,为此微软根据设备自身的需求对.NET Framework作了相应的简化和改变,不断推出了针对具体设备类型的.NET Framework,主流的包括Windows Phone.Windows Store.Silverlight和Xbox等,它们分别对移动.

最大限制地提高代码的可重用性

    重用是一种神话,这似乎正在日渐成为编程人员的一种共识.然而,重用可能难以实现,因为传统面向对象编程方法在可重用性方面存在一些不足.本技巧说明了组成支持重用的一种不同方法的三个步骤. 第一步:将功能移出类实例方法由于类继承机制缺乏精确性,因此对于代码重用来说它并不是一种最理想的机制.也就是说,如果您要重用某个类的单个方法,就必须继承该类的其他方法以及数据成员.这种累赘不必要地将要重用此方法的代码复杂化了.继承类对其父类的依赖性引入了额外的复杂性:对父类的更改会影响子类:当更改父类或子类中的

最大限制地提高代码的可重用性,克服传统面向对象编程方法在可重用性方面的不足

编程|对象     重用是一种神话,这似乎正在日渐成为编程人员的一种共识.然而,重用可能难以实现,因为传统面向对象编程方法在可重用性方面存在一些不足.本技巧说明了组成支持重用的一种不同方法的三个步骤. 第一步:将功能移出类实例方法由于类继承机制缺乏精确性,因此对于代码重用来说它并不是一种最理想的机制.也就是说,如果您要重用某个类的单个方法,就必须继承该类的其他方法以及数据成员.这种累赘不必要地将要重用此方法的代码复杂化了.继承类对其父类的依赖性引入了额外的复杂性:对父类的更改会影响子类:当更改父

《C++代码设计与重用》导读

前言 C++代码设计与重用 一切事物都将得到检验并因此被称为问题. Edith Hamilton 这本书的主要目的在于:展示如何以C++编程语言编写可重用代码-就是说,根据不同的需要,在不经过修改,或者经过很少修改的前提下,可重用代码可以很容易地应用到5个.50个甚至500个程序当中,而且这些程序往往是不同程序员编写的,可能运行在不同的系统上.在整个阐述的过程中,我们的目的并不在于争论是否所有的代码都是可重用的,也不在于说明可重用代码能够解决所有的程序问题.显然,不论是对程序员而言,还是对可重用

《C++代码设计与重用》——2.3 Nice类

2.3 Nice类 C++代码设计与重用 2.3 Nice类 我们都知道类会提供某些函数,这些函数要么是在类的代码中被显式声明为公共的(public)或保护的(protected),要么是由编译器在程序需要这些代码时隐式生成的.例如,下面这个类: class X{ public: X(); void f(); }; 它提供了一个缺省构造函数.函数f.一个拷贝构造函数.一个赋值运算符和一个析构函数.而且最后3个函数会在程序需要它们的时候由编译器自动生成. 请考虑下面这个通常有用的函数: templ

《C++代码设计与重用》——1.5 这本书能给我们带来什么

1.5 这本书能给我们带来什么 C++代码设计与重用 1.5 这本书能给我们带来什么 编写可重用代码可以使复杂的问题变得比较简单,但编码过程是非常困难的.这本书不会也不能让这困难的过程变得格外简单,这本书也没有提供能让每个C++程序员都可以很轻松地编写出可重用代码的锦囊妙计. 针对每个希望编写出可重用代码的C++程序员,这本书的每一章都讨论了一个或者多个他们必须理解的问题.理解了这些问题虽然不能使编写可重用代码变得相当简单,但可以让编写出可重用代码成为一种可能. 这本书的其余部分的结构如下: 当

《C++代码设计与重用》——1.2 重用的神话

1.2 重用的神话 C++代码设计与重用1.2 重用的神话关于代码重用出现了许多神话(荒诞的说法),这一节我们来反驳几个比较普遍的说法. 神话1:重用可以解决软件危机 软件危机是指程序设计团体现今没有能力做到以下几点:编写解决复杂问题的程序,快速生成解决复杂问题的程序,正确编写这些程序并使这些程序的维护相当容易. 软件开发进步的迹象是显而易见的.一个很显然的迹象就是随着时间的推移,所谓的复杂问题的范围发生了改变.在20世纪60年代,编写一个FORTRAN-66编译器就被认为是一个非常复杂的问题:

《C++代码设计与重用》——1.1 什么是重用性

1.1 什么是重用性 C++代码设计与重用 1.1 什么是重用性 许多相同操作都会在多个计算机程序里重复实现,例如: 对数组元素进行排序:解答线性方程组:实现一个从X类型到Y类型的映射:解析C++代码:从数据库检索数据:和其他程序进行通信.与其在每个程序里都设计和实现上面每个操作的相同代码,我们更愿意采用的方法是:只设计和实现这些操作的代码一次,然后再把这些代码重用手不同程序里.显然,已有的可重用代码,使每个应用程序不必从头写起,因为它(可重用代码)大大加速了应用程序的开发,并且减少了编写和维护

《C++代码设计与重用》——2.7 转型

2.7 转型 C++代码设计与重用2.7 转型程序库设计者必须充分重视隐式转型(implicit conversion).在C++中,有两种方法可以用来定义从类型From到类型To的隐式转型.第一种,我们可以在类To中定义一个只含一个参数的构造函数(并且没有其他的缺省参数): class To { public: To(const From&); //或者是To(From) //... }; 或者,我们可以在类From中定义一个转型操作: class From { public: operato