为了部署CLR模块,开发人员首先必须将其归属于一个程序集(assembly)。程序集就是一个或多个模块的逻辑集合。如前面讨论过的那样,模块是以字节流形式存在的物理构件,通常存放在文件系统中。程序集是逻辑构件,并且通过独立于位置的名字进行引用。而这个名字必须翻译为文件系统中或Internet上的物理路径。那些物理路径最终指向一个或多个包含类型定义、代码以及资源的模块。
CLR允许开发人员由多个模块组建程序集,主要是为了支持将那些不经常访问的代码的加载区分开来,同时不用为它们形成单独的封装边界。这个特征在开发人员采用代码下载时特别有用,因为他们可以先只下载初始模块,根据需求才会下载下一个模块。多模块程序集还可以是混合语言的。这样,开发人员既可以采用高生产率的语言(例如,Logo.NET),用于完成大部分工作,同时,采用更为灵活的语言(例如,C++)编写底层代码。通过将这两个模块结合为单个程序集,开发人员能够同时将C++和Logo.NET代码作为一个原子单元进行引用、部署以及版本控制。
尽管程序集可能由多个模块组成,但是一个模块往往只属于一个程序集。假如出现两个程序集都引用一个公共模块的情况,将作如何处理?这时,CLR将这个公共模块视为两个不同的模块,即公共模块中的每个类型都有两个不同的拷贝。基于上述理由,本章剩余部分将假定一个模块只明确地属于一个程序集。
在CLR中,程序集是部署的“原子”,被用来对CLR模块进行打包、加载、分布以及版本控制。虽然程序集可能包括多个模块以及辅助文件,但程序集本身被作为原子单元进行命名和版本化。如果程序集的某个模块版本发生变化,那么,整个程序集必须重新部署,因为版本号是程序集名字的一部分,而不是底层模块名字的一部分。
模块一般都依赖于来自其他程序集的类型。最起码每个模块都依赖于定义在mscorlib程序集中的类型,例如,System.Object和System.String等等。每个CLR模块都包含一个程序集名字的列表,指明该模块所使用的程序集。对于这些程序集以外的引用,它们只是使用了程序集的逻辑名字,而不包含底层模块名或者位置信息。CLR将负责在运行时将这些程序集的逻辑名字转换为模块的路径名。本章后面还将专门讨论。
为了促使CLR能够找到程序集中不同部分,每个程序集都正好有一个模块,其元数据包含了程序集清单(assembly manifest)。程序集清单是CLR元数据中附加的一部分,相当于附加的类型定义和代码的附属文件目录。CLR能够直接加载包含程序集清单的模块。对于没有程序集清单的模块,CLR只能先加载含有程序集清单的模块,并且,该清单引用了这些没有清单的模块,从而间接地加载它们。图2.2展示了两个模块:一个含有程序集清单,一个则没有。注意4个/t编译选项,只有/t:module产生没有程序集清单的模块。
图2.3展示了一个使用多模块程序集的应用程序,示例2.1则是产生它的MAKEFILE文件内容。在这个例子中,code.netmodule就是不含程序集清单的模块。为了让这个模块有用,就需要第二个模块(本例为component.dll)提供一个程序集清单,并将code.netmodule作为下级模块进行引用。当编译所包含的程序集时,要使用/addmodule开关。在这个程序集生成之后,所有在component.dll和code.netmodule中定义的类型都通过程序集的名字(component)确定作用域。应用程序(例如,Application.exe)使用/r编译选项,来引用含有程序集清单的模块。这样,使得两个模块中的类型都能为程序所用。
图2.2:模块和程序集
图2.3:使用CSC.EXE编译多模块程序集
示例2.1:使用CSC.EXE和NMAKE编译多模块程序集
# code.netmodule cannot be loaded as is until an assembly
# is created
code.netmodule : code.cs
csc /t:module code.cs
# types in component.cs can see internal and public members
# and types defined in code.cs
component.dll : component.cs code.netmodule
csc /t:library /addmodule:code.netmodule component.cs
# types in Application.cs cannot see internal members and
# types defined in code.cs (or component.cs)
Application.exe : Application.cs component.dll
csc /t:exe /r:component.dll Application.cs
程序集清单存放在一个明确的模块中,并且包含了用于定位类型和资源的所有信息,而这些类型和资源则被定义为程序集的一部分。图2.4展示了被组合成单个程序集的一组模块,在构建它们时需要相应的CSC.EXE开关。注意在这个例子中,程序集清单包含对下级模块pete.netmodule和george.netmodule的文件引用列表。除了这些文件引用之外,这些下级模块的每个公共类型都通过.class extern指令列出来。这样,就有了公共类型的完整列表,而不用对程序集中每个模块都遍历元数据。列表的各项指明了类型所在的文件名,以及唯一标识模块中类型的数值的元数据标记(numeric metadata token)。
图2.4:多模块程序集
最后,含有程序集清单的模块将包含外部引用程序集的主要列表。列表由程序集中每个模块的依赖关系(dependency)组成,而不仅仅是当前模块的依赖关系。这样,通过加载单个文件,就能找到程序集所有的依赖关系。
程序集形成一个封装边界(encapsulation boundary),在程序集之间的访问中保护内部实现细节。程序员可以对类型的成员(例如,字段、方法、构造函数等)实施保护,也可以保护整个类型。将类型成员标注为internal,将导致它只对同一程序集的模块是可用的。假如将类型成员标注为public,则导致它对所有代码(当前程序集内部以及外部)是可用的。假如类型中单独成员(例如,方法、字段、构造函数)还能标注为private,只有该声明类型中的方法和构造函数才能访问。这样对于组件内部的封装,编程上与传统的C++风格一致。类似的情形,程序员能将类型成员标注为protected,它放宽了private所允许的访问限制,使得派生类型的方法和构造函数也能访问该成员。访问修饰符protected和internal可以组合在一起使用,这样既能够访问当前类型派生的类型,也能够访问同一程序集中的类型。表2.2展示了特定语言修饰符运用到类型以及单独成员中的情形。注意在C#中,标注为protected internal的成员要么只对同一程序集中的访问方法开放,要么只对派生类型的访问方法开放。CLR还支持一种访问修饰符(在元数据中的标注为famandassem),既对同一程序集中的访问方法公开,又对派生类型的访问方法公开。不过,VB.NET和C#并不允许程序员指定这种访问修饰符。
表2.2 访问修饰符
|
C# |
VB.NET |
意义 |
类型 |
public |
Public |
访问类型不受限制 |
internal |
Friend |
类型只在程序集内部可访问 |
成员 |
public |
Public* |
访问成员不受限制 |
internal |
Friend |
成员只在程序集内部可访问 |
protected |
Protected |
访问仅限于包含类或者从包含类派生的子类型 |
protected internal |
Protected Friend |
访问仅限于包含类以及从包含类派生的子类型,或者当前程序集的其他类型 |
Private |
Private* |
访问仅限于包含类型 |
* 在VB.NET中,通过关键字Dim声明的方法默认为Public,而字段默认为Private。
程序集中定义类型Customer,而在运行时却不发生混淆,但是,这并不能帮助程序员在单个程序中使用两个或多个同名的类型定义。因为符号化的类型名总是Customer,而不管哪个程序集定义它。为了解决这种大多数编程语言的限制,CLR类型名会有一个命名空间前缀(namespace prefix)。这个前缀是一个字符串,一般以开发人员的组织名(例如,Microsoft、AcmeCorp)开始;如果是.NET Framework的一部分话,则以System开始。程序集的命名约定通常是基于命名空间前缀。例如,.NET XML堆栈被部署在System.Xml程序集中,它包含的所有类型都使用System.Xml的命名空间前缀。这仅仅是一个约定,而不是规则。例如,类型System.Object存放在名为mscorlib程序集中,而不是名为System的程序集中,尽管也确实存在名为System的程序集。