当你指定下面任何一个命令行开关时, C#编译器都产生一个程序集: /t[arget]:exe, /t[arget]:winexe, 或者/t[arget]:library. 这些开关都会使得编译器产生单独的PE文件, 其包含着manifest metadata表, 产生的文件或者CUI可执行程序, 或者是GUI可执行程序, 或者是一个DLL.
除了这些开关, C#编译器支持/t[arget]:module开关, 这个开关告诉编译器产生一个不包含manifest metadata表的PE文件, 产生的PE文件总是一个DLL PE文件, 这个文件必须被加入到一个程序集中, CLR 才能访问它中的类型. 当你使用/t:module开关时, C#编译器默认情况下会在输出文件中增加一个.netmodule扩展名.
重要: 不幸的是, 微软的Visual Studio集成开发环境(IDE)不支持创建多文件程序集, 如果你想创建多文件程序集, 你必须求助于命令行工具.
有很多方法可以给一个程序集增加模块, 如果你使用C#编译器来构建带有manifest的PE文件, 你可以使用/addmodule开关. 为了理解如何构建一个多文件程序集, 让我们假设你有两个源代码文件:
RUT.cs: 包含很少使用的类型
FUT.cs: 包含经常使用的类型
让我们把很少使用的类型编译为他们自己的模块, 使得程序集的用户不需要部署这个模块, 如果用户不需要访问这个很少使用的类型.
csc /t:module RUT.cs
这行命令使C#编译器创建一个RUT.netmodule文件, 这个文件是一个标准的DLL PE文件, 但是, 仅仅通过这一个文件, CLR还不能载入它.
下面让我们编译经常使用的类型为他们自己的模块, 我们将使得这个模块保存程序集的manifest, 因为这个模块中的类型是经常被使用的. 实际上, 因为这个模块将会代表整个程序集, 我将改变输出文件的名字为JeffTypes.dll, 而不是默认的FUT.dll.
csc /out:JeffTypes.dll /t:library /addmodule:RUT.netmodule FUT.cs
这行命令告诉C#编译器编译FUT.cs文件并产生JeffTypes.dll文件. 因为指定了/t:library, 一个包含manifest metadata表的DLL PE文件被放到JeffTypes.dll文件中. /addmodule:RUT.netmodule开关告诉编译器RUT.netmodule是一个文件, 这个文件应该被认为是程序集的一部分. 特别地, /addmodule开关告诉编译器在FileDef manifest metadata表中增加一个文件, 把RUT.netmodule的公开暴露的类型加入到ExportedTypesDef manifest metadata表中.
当编译器完成所有这些操作时, 在图2-1中显示了创建的两个文件, 在右边的模块包含了manifest.
图2-1 多文件程序集: 包含两个托管模块, 其中一个包含着manifest
RUT.netmodule文件包含着IL代码(通过编译RUT.cs所产生的), 这个文件也包含着metadata表, 其描述了RUT.cs中定义的类型, 方法, 字段, 属性, 事件, 等. 这个metadata标也描述了RUT.cs中引用的类型, 方法, 等. JeffTypes.dll是一个单独的文件, 类似于RUT.netmodule, 这个文件包含着IL代码(编译FUT.cs所产生的), 这个文件包含着类似的定义和引用metadata表. 然而, JeffTypes.dll还包含着额外的manifest metadata表, 使得JeffTypes.dll是一个程序集. 这个额外的manifest metadata表描述了组成程序集的所有的文件(JeffTypes.dll文件自身和RUT.netmodule文件). manifest metadata表也包含了所有JeffTypes.dll和RUT.netmodule公开暴露的类型.
注意: 实际上, manifest metadata表中没有”包含着manifest的PE文件”中暴露的类型, 这个优化的目的是为了减小PE文件中manifest信息的字节数量, 所以陈述” manifest metadata表也包含了所有JeffTypes.dll和RUT.netmodule公开暴露的类型”不是100%的准确. 然而这个陈述准确地反映了manifest在逻辑上所暴露的内容.
当构建了JeffTypes.dll程序集, 你可以使用IlDasm.exe来检查metadata的manifest表来验证程序集文件真的包含着RUT.netmodule文件中的类型. 下面是FileDef和ExportedTypesDef metadata表的样子:
File #1 (26000001)
-----------------------------------------------------
Token: 0x26000001
Name : RUT.netmodule
HashValue Blob : e6 e6 df 62 2c al 2c 59 97 65 0f 21 44 10 15 96 f2 7e db c2
Flags : [ContainsMetaData] (00000000)
ExportedType #1 (27000001)
-----------------------------------------------------
Token: 0x27000001
Name: ARarelyUsedType
Implementation token: 0x26000001
TypeDef token: 0x02000002
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
[BeforeFieldlnit](00100101)
从这个例子可以看出, RUT.netmodule是一个文件, 是程序集的一部分, 其token是0x26000001. 从ExportedTypesDef表中, 可以看到有一个公开暴露的类型ARarelyUsedType, 这个类型的实现token是0x26000001, 这表明这个类型的IL代码包含在RUT.netmodule文件中.
注意: 出于好奇, metadata token是一个4字节的数值, 高位字节表示token的类型(0x01=TypeRef, 0x02=TypeDef, 0x23=AssemblyRef, 0x26=FileRef, 0x27=ExportedType). 完整的列表可以参考CorHdr.h中CorTokenType枚举的类型, 这个文件是.NET Framework SDK中包含的文件. 三个低位字节简单地表明了在对应的metadata表中的行. 例如, 实现token 0x000001引用了FileRef表的第一行. 对大多数表来说, 行是从1开始计数的, 而不是0. 对于TypeDef表, 行是从2开始计数的.
任何client代码要使用JeffTypes.dll程序集的类型, 它必须用/r[eference]: JeffTypes.dll编译器开关来构建. 这个开关告诉编译器载入JeffTypes.dll程序集以及在它的FileDef表中列出的所有文件(当搜索外部类型时). 编译器需要所有的程序集文件, 如果你删除了RUT.netmodule文件, C#编译器将会产生如下错误: "fatal error CS0009: Metadata file 'C:/JeffTypes.dll could not be opened—'Error importing module 'RUT.netmodule' of assembly 'C:/JeffTypes.dll'—The system cannot find the file specified'". 这意味着为了构建一个新的程序集, 被引用的程序集中的所有文件都必须存在.
当client代码执行时, 它会调用相应的函数. 如果函数是第一次调用, 那么CLR会检查函数引用的参数类型, 返回值类型, 以及局部变量的类型. 然后CLR尝试载入被引用的程序集文件(包含着manifest的程序集). 如果正在被访问的类型在这个文件中, 那么CLR执行内部的薄记, 允许使用类型. 如果manifest表明被引用的类型在一个不同的文件中, CLR尝试载入必要的文件, 执行内部的薄记, 并允许访问类型. 只有在一个方法引用一个类型,并且这个类型是在一个没有被载入的程序集中时, CLR才载入相应的程序集文件. 这意味着, 运行一个应用程序, 被引用程序集中的所有文件不必都存在.