程序集(assembly)是包含编译好的、面向.NET Framework的代码的逻辑单元。程序集是完全自我描述性的,也是一个逻辑单元而不是物理单元,它可以存储在多个文件中(动态程序集的确存储在内存中,而不是存储在文件中)。如果一个程序集存储在多个文件中,其中就会有一个包含入口点的主文件,该文件描述了程序集中的其他文件。
注意可执行代码和库代码使用相同的程序集结构。惟一的区别是可执行的程序集包含一个主程序入口点,而库程序集则不包含。
程序集的一个重要特性是它们包含的元数据描述了对应代码中定义的类型和方法。程序集也包含描述程序集本身的元数据,这种程序集元数据包含在一个称为程序集清单的区域中,可以检查程序集的版本及其完整性。
注意:
ildasm是一个基于Windows的实用程序,可以用于检查程序集的内容,包括程序集清单和元数据。第15章将介绍ildasm。
程序集包含程序的元数据,表示调用给定程序集中的代码的应用程序或其他程序集不需要指定注册表或其他数据源,以便确定如何使用该程序集。这与以前的COM有很大的不同,以前,组件的GUID和接口必须从注册表中获取,在某些情况下,方法和属性的详细信息也需要从类型库中读取。
把数据分散在3个以上的不同位置上,可能会出现信息不同步的情况,从而妨碍其他软件成功地使用该组件。有了程序集后,就不会发生这种情况,因为所有的元数据都与程序的可执行指令存储在一起。注意,即使程序集存储在几个文件中,数据也不会出现不同步的问题。这是因为包含程序集入口的文件也存储了其他文件的细节、散列和内容,如果一个文件被替换,或者被塞满,系统肯定会检测出来,并拒绝加载程序集。
程序集预览(Assembly Overview)
任何.NET二进制代码使用的.NET运行时是一个或一组程序集组成的。当你编译一个应用程序时,实际上是在创建一个程序集。
任何时候构建一个EXE或DLL文件时必须使用/t:library编译参数创建与该应用程序相对应的包含清单(manifest)的程序集,清单(manifest)记录了.NET运行时程序集的相关信息。另外,使用/t:module编译参数,创建一个以.netmodule为扩展名的DLL 文件,这个DLL文件不包含清单。换句话说,虽然逻辑上模块(module)依然是一个DLL文件,但是它不属于程序集,当编译应用程序或使用程序集生成工具(the Assembly Generation tool)时必须使用/addmodule开关将这个DLL文件加入到其他程序集中。
清单数据(Manifest Data)
程序集的清单有不同的方法存储。若编译一个独立的应用程序或DLL文件,清单与程序或DLL的PE结合在一起,这称为单一文件的程序集(single-file assembly)。多文件程序集(multifile assembly)是指清单以另外一个独立实体的形式作为程序集的一部分,或者作为程序集中的一个模块。
同样程序集的定义很大程度取决你如何使用它。从客户的角度来看,程序集是模块、导出类型、(必需、可选)资源的命名和版本化(versioned)的集合。从程序集创建者的角度来看,程序集是客户能使用的一组相关联的模块、类型、资源、导出的方法的封装包(mean of packaging)。也就是说,清单是程序集和用户之间沟通的桥梁。下面列出程序集中清单包含的信息:
· 程序集名称(Assembly name)
· 版本信息(Versioning information)
版本信息是由四个不同的部分组成的版本号,分别是主版号、子版号、构建序号、修订号。
· (可选的)共享名和带符号的程序集散列(An(Optional)shared name and signed assembly hash)
主要是关于程序集部署的相关信息。
· 文件(Files)
包含在程序集中的文件列表。
· 引用的程序集(Referenced assemblies)
直接引用的外部程序集列表。
· 类型(Types)
程序集内部的所有类型及模块映像包含的类型的列表。(This is list of all types in the assembly with a mappig to containing the type.)
· 安全(Security)
安全权限的列表,权限已明确地被程序集抛弃。(permissions This is a list of security permissions that are explicity refused by the assembly.)
· 定制属性(Custom attributes)
· 产品信息
包含公司、商标、产品及版权信息。
程序集的优势
· 程序集的封装:将多个模块封装到一个物理文件,起到性能优化的作用。当你创建一个应用程序并使用多文件程序集时,.NET运行时只需加载相关的模块。这种策略起到减少应用程序的工作集。
· 程序集的部署
在.NET框架中程序集是最小的部署单元。虽然它诱人的说,程序集是部署应用程序的方法,但是技术上并不是如此。许多关于程序集部署的正确的观点是:在.NET中程序集应该看作一个窗体的类部署(class deployment)-就像Win32中的一个DLL文件。一个单独的应用程序是由多个程序组成的。
因为程序集是自描述的(self-desciribing),所以部署程序集最简单的方法就是直接把程序集复制到目的文件夹。当尝试运行包含这个程序集的应用程序时,清单将向.NET运行时提供包含在这个程序集中的方法信息。另外,清单(manifest)也向应用程序提供该程序集所引用的外部程序集的相关信息。
许多通过公共方法部署的程序集依然是私有的程序集,即程序集被复制到文件夹,但它们不是共享的。缺省情况下程序集是私有的,除非明确地指定程序集为共享程序集(shared assembly)。
· 程序集的版本
使用程序集另外一个主要优势是程序集内置了版本号。程序集中止了DLL地狱。DLL地狱是指当一个应用程序重写了一个被其他应用程序引用的DLL文件,而该应用程序引用的是低版本的同名DLL文件,这就造成了引用低版本DLL的应用程序运行出错,这种情况。虽然Win32资源文件格式内置了版本资源类型,但是操作系统不会强制执行任何版本控制,以便依赖这个DLL文件的应用程序能正常运行。
基于这个问题,清单不仅包含程序集本身的版本信息,也包含了该程序集所有被引用的程序集和这些程序集的版本信息。因为有了这个体系结构,.NET运行时能确保版本规则被支持,当出现新版本的程序集时,版本不兼容的共享DLLs由操作系统自动安装,使得应用程序可以继续正常运行。
程序集有两种类型:共享程序集和私有程序集。
私有程序集
私有程序集是最简单的一种程序集类型。私有程序集一般附带在某些软件上,且只能用于该软件中。附带私有程序集的常见情况是,以可执行文件或许多库的方式提供应用程序,这些库包含的代码只能用于该应用程序。
系统可以保证私有程序集不被其他软件使用,因为应用程序只能加载位于主执行文件所在文件夹或其子文件夹中的程序集。
用户一般会希望把商用软件安装在它自己的目录下,这样软件包没有覆盖、修改或加载另一个软件包的私有程序集的风险。私有程序集只能用于自己的软件包,这样,用户对什么软件使用它们就有了更多的控制。因此,不需要采取安全措施,因为这没有其他商用软件用某个新版本的程序集覆盖原来的私有程序集的风险(但软件是专门执行怀有恶意的损害性操作的情况除外)。名称也不会有冲突。如果私有程序集中的类正巧与另一个人的私有程序集中的类同名,是不会有问题的,因为给定的应用程序只能使用私有程序集的名称。
因为私有程序集完全是自含式的,所以安装它的过程就很简单。只需把相应的文件放在文件系统的对应文件夹中即可(不需要注册表项),这个过程称为“0影响(xcopy)安装”。
共享程序集
共享程序集是其他应用程序可以使用的公共库。因为其他软件可以访问共享程序集,所以需要采取一定的保护措施来防止以下风险:
● 名称冲突,另一个公司的共享程序集执行的类型与自己的共享程序集中的类型同名。因为客户机代码理论上可以同时访问这些程序集,所以这是一个严重的问题。
● 程序集被同一个程序集的不同版本覆盖——新版本与某些已有的客户机代码不兼容。
这些问题的解决方法是把共享程序集放在文件系统的一个特定的子目录树中,称为全局程序集高速缓存(GAC)。与私有程序集不同,不能简单地把共享程序集复制到对应的文件夹中,而需要专门安装到高速缓存中,这个过程可以用许多.NET工具来完成,其中包含对程序集的检查、在程序集高速缓存中设置一个小的文件夹层次结构,以确保程序集的完整性。
为了避免名称冲突,共享程序集应根据私有密钥加密法指定一个名称(私有程序集只需要指定与其主文件名相同的名称即可)。该名称称为强名(strong name),并保证其惟一性,它必须由要引用共享程序集的应用程序来引用。
与覆盖程序集相关的问题,可以通过在程序集清单中指定版本信息来解决,也可以通过同时安装来解决。
若要创建一个在多个应用程序使用的程序集,并且版本信息相当重要,则必须共享程序集。若要共享程序集,必须给程序集指定强名称。可以通过.NET SDK提供强名称工具(the Strong Name tool)创建强名称。使用强名称有四个主要理由:
· 它是.NET生成唯一的全局名称的机制
· 因为强名称工具生成的密钥对包含了一个签名,你可以判断程序集创建之后是否被篡改。(Because the generated key pair includes a singature,you can tell whether an assembly has been tampered with after its original creation.)
· 强名称能防止第三方在你构建的程序集的基础上发表新的版本。这是因为密钥对包含签名起了作用,因为第三方不知道你的私钥。
· 当.NET加载程序集时,运行时会自动验证调用者是否合法。(When .NET load an assembly,the runtime can verify that the assembly came from the publisher that the caller is expecting.)
创建强名称的步骤:
· 使用强名称工具为程序集创建一个强名称。示例:
sn -k InsideCSharp.key
· 在客户端源代码文件中添加属性AssemblyKeyFile,把强名称指定给程序集。
使用全局程序集缓存工作
.NET每次加载程序集时都会创建一个代码缓存区,通常成为全局程序集缓存。使用全局程序集缓存有三个作用:
· 存储从Internet或文件服务器下载的代码。注:从一个特定的应用程序加载的代码存储在缓冲中的私有区域,以防止其他程序集访问。
· 存储被多个.NET应用程序共享的组件数据。利用全局缓存工具将程序集加载到缓存全局区域,使得该程序集可以被本地机器的所有应用程序访问。
· 程序集的本地代码是预运行时编译的,同时存储在全局程序集缓存区。(native code versions of assemblies at have been preJITted are stored in the cache.)
反射
因为程序集存储了元数据,包括在程序集中定义的所有类型和这些类型的成员的细节,所以可以编程访问这些元数据。这个技术称为反射,第11章详细介绍了它们。该技术很有趣,因为它表示托管代码实际上可以检查其他托管代码,甚至检查它自己,以确定该代码的信息。它们常常用于获取特性的详细信息,也可以把反射用于其他目的,例如作为实例化类或调用方法的一种间接方式,如果把方法上的类名指定为字符串,就可以选择类来实例化方法,以便在运行时调用,而不是在编译时调用,例如根据用户的输入来调用(动态绑定)。