2Net本质论中文版.docx
- 文档编号:9777027
- 上传时间:2023-02-06
- 格式:DOCX
- 页数:35
- 大小:436.27KB
2Net本质论中文版.docx
《2Net本质论中文版.docx》由会员分享,可在线阅读,更多相关《2Net本质论中文版.docx(35页珍藏版)》请在冰豆网上搜索。
2Net本质论中文版
第二章 组件
CLR对组件代码的打包、部署和查找有自己一整套的概念和技术。
这些概念和技术与COM、Java和Win32存在着根本上的差别。
如果进一步认识CLR加载器,就能够很好地理解它们之间的差异。
不过,我们必须先知道代码和元数据是如何打包的。
模块定义
CLR程序存在模块(module)中。
一个CLR模块是一个字节流,通常作为一个文件存储在本地的文件系统中或者Web服务器上。
如图2.1所示,CLR模块采用WindowsNT的PE/COFF可执行文件格式的扩展版。
不过,CLR对PE/COFF文件格式进行了很大的扩展,而不是简单的沿袭。
同时,CLR模块也是有效的Win32模块,可以通过LoadLibrary系统调用进行加载。
不过,CLR模块用到的PE/COFF的功能极少。
准确地说,CLR模块的大部分内容是作为不透明的数据,存放在PE/COFF文件的.text部分。
CLR模块包含代码、元数据和资源。
代码一般以公共中间语言[commonintermediatelanguage(CIL)]的格式存放(尽管代码也可能被存为特定处理器的机器指令)。
模块的元数据描述了模块中定义的类型,包含名字、
PE头
COFF头
IMAGE_COR20_HEADER
代码
(CIL和/或本机机器码
资源数据
字符串/BLOB池
元数据表
图2.1:
CLR模块格式
继承关系、方法签名和依赖信息等。
模块的资源由静态的只读数据组成,例如,字符串、位图,以及其他没有被存储为可执行代码的部分。
CLR模块使用的文件格式具有较好的文档性,开发人员很少也会遇到未加工的格式。
因此,即使对于急于求成的开发人员,一般也能够使用CLR提供的两个实用部件中的一个,用来进行可编程地生成模块。
IMetaDataEmit接口是低级的COM接口,可以用来由经典的C++编程生成模块元数据。
System.Reflection.Emit命名空间是高级的类库,用来由任何CLR正式语言(例如,C#、VB.NET)编程生成元数据和CIL。
CodeDOM则工作在更高级的抽象层面上,不必知道和理解CIL。
然而,对绝大多数开发人员来说,他们只是需要在开发时生成代码,而不是运行时,对此CLR编译器完全能够胜任。
C#编译器(CSC.EXE)、VB.NET编译器(VBC.EXE)和C++编译器(CL.EXE)都能够将源代码翻译成CLR模块。
各个编译器通过命令行开关控制产生的
表2.1 模块输出选项
C#/VB.NET
C++
直接可加载的?
从Shell中可直接运行?
可访问控制台?
/t:
exe
/CLR
是
是
总是
/t:
winexe
/CLR/link
/subsystem:
windows
是
是
从不
/t:
library
/CLR/LD
是
否
依赖主机(Host-dependent)
/t:
module
/CLR:
NOASSEMBLY
/LD
否
否
依赖主机(Host-dependent)
模块种类。
如表2.1所示,有4个可能的选项。
在C#和VB.NET中,通过/target命令行开关(或者其快捷形式/t)选择目标文件的种类。
C++编译器可以使用多个开关的组合;不过,通过/CLR开关,强制C++编译器生成CLR兼容的模块。
下面所用到的C#和VB.NET开关,将采用它们的快捷形式。
选项/t:
module产生“未加工的(raw)”模块,其文件扩展名默认为.netmodule。
这种格式的模块不能独立地部署,CLR也不能直接加载它们。
准确地说,开发人员必须在部署前,将这些“未加工的”模块与成型的组件(被称为程序集)进行关联。
相比之下,用/t:
library选项编译产生的模块,能够包含附加的元数据,允许开发人员将其作为独立代码进行部署。
选项/t:
library编译产生的模块,其文件扩展名默认为.DLL。
用/t:
library编译产生的模块能被CLR直接加载,但不能从命令外壳或Windows资源管理器中作为可执行程序启动。
如果要产生可执行程序,你必须采用/t:
exe或者/t:
winexe选项。
这两个选项均产生扩展名为.EXE的文件,唯一的差别是:
前者假定为控制台UI子系统使用,后者则假定为GUI子系统。
如果没有指定/t选项,默认为/t:
exe。
不管是使用/t:
exe还是/t:
winexe选项产生的模块,都必须定义一个初始入口点(initialentrypoint)。
初始入口点是程序启动时CLR将自动执行的方法。
程序员必须将这个方法声明为static,并且,在C#或VB.NET中,还必须命名为Main。
程序员能够将入口点方法声明为无返回值,或者返回int型值。
他们也可以将其声明为无参数形式,或者接受一个字符串数组的参数,它包含从外壳程序输入的命令行参数。
下面是C#中Main方法的四种合法的实现。
staticvoidMain(){}
staticvoidMain(string[]argv){}
staticvoidMain(){return0;}
staticvoidMain(string[]argv){return0;}
对应的VB.NET代码为:
sharedsubMain():
endsub
sharedsubMain(argvasstring()):
endsub
sharedfunctionMain():
return0:
endfunction
sharedfunctionMain(argvasstring())
return0
endfunction
注意,这些方法并不是必须声明为public。
不过,程序员只能在类型定义中声明Main方法,尽管类型名称并不重要。
下面是最小的C#程序,只是向控制台打印字符串“Hello,world”。
classmyapp{
staticvoidMain(){
System.Console.WriteLine("Hello,World");
}
}
在这个例子中,只有一个类,其中包含一个名为Main的静态方法。
如果源文件包含多个类型,都有名为Main的静态方法,那么,C#或VB.NET编
译可能无所适从(甚至导致错误)。
为了解决这种二义性,程序员可以采用/main命令行开关,告诉C#或VB.NET编译器哪个类型将用作程序的初始入口点。
程序集定义
为了部署CLR模块,开发人员首先必须将其归属于一个程序集(assembly)。
程序集就是一个或多个模块的逻辑集合。
如前面讨论过的那样,模块是以字节流形式存在的物理构件,通常存放在文件系统中。
程序集是逻辑构件,并且通过独立于位置的名字进行引用。
而这个名字必须翻译为文件系统中或Internet上的物理路径。
那些物理路径最终指向一个或多个包含类型定义、代码以及资源的模块。
尽管程序集可能由多个模块组成,但是一个模块往往只属于一个程序集。
假如出现两个程序集都引用一个公共模块的情况,将作如何处理?
这时,CLR将这个公共模块视为两个不同的模块,即公共模块中的每个类型都有两个不同的拷贝。
基于上述理由,本章剩余部分将假定一个模块只明确地属于一个程序集。
CLR允许开发人员由多个模块组建程序集,主要是为了支持将那些不经常访问的代码的加载区分开来,同时不用为它们形成单独的封装边界。
这个特征在开发人员采用代码下载时特别有用,因为他们可以先只下载初始模块,根据需求才会下载下一个模块。
多模块程序集还可以是混合语言的。
这样,开发人员既可以采用高生产率的语言(例如,Logo.NET),用于完成大部分工作,同时,采用更为灵活的语言(例如,C++)编写底层代码。
通过将这两个模块结合为单个程序集,开发人员能够同时将C++和Logo.NET代码作为一个原子单元进行引用、部署以及版本控制。
在CLR中,程序集是部署的“原子”,被用来对CLR模块进行打包、加载、分布以及版本控制。
虽然程序集可能包括多个模块以及辅助文件,但程序集本身被作为原子单元进行命名和版本化。
如果程序集的某个模块版本发生变化,那么,整个程序集必须重新部署,因为版本号是程序集名字的一部分,而不是底层模块名字的一部分。
模块一般都依赖于来自其他程序集的类型。
最起码每个模块都依赖于定义在mscorlib程序集中的类型,例如,System.Object和System.String等等。
每个CLR模块都包含一个程序集名字的列表,指明该模块所使用的程序集。
对于这些程序集以外的引用,它们只是使用了程序集的逻辑名字,而不包含底层模块名或者位置信息。
CLR将负责在运行时将这些程序集的逻辑名字转换为模块的路径名。
本章后面还将专门讨论。
为了促使CLR能够找到程序集中不同部分,每个程序集都正好有一个模块,其元数据包含了程序集清单(assemblymanifest)。
程序集清单是CLR元数据中附加的一部分,相当于附加的类型定义和代码的附属文件目录。
CLR能够直接加载包含程序集清单的模块。
对于没有程序集清单的模块,CLR只能先加载含有程序集清单的模块,并且,该清单引用了这些没有清单的模块,从而间接地加载它们。
图2.2展示了两个模块:
一个含有程序集清单,一个则没有。
注意4个/t编译选项,只有/t:
module产生没有程序集清单的模块。
图2.3展示了一个使用多模块程序集的应用程序,示例2.1则是产生它的MAKEFILE文件内容。
在这个例子中,module就是不含程序集清单的模块。
为了让这个模块有用,就需要第二个模块(本例为component.dll)提供一个程序集清单,并将module作为下级模块进行引用。
当编译所包含的程序集时,要使用/addmodule开关。
在这个程序集生成之后,所有在component.dll和module中定义的
图2.2:
模块和程序集
图2.3:
使用CSC.EXE编译多模块程序集
类型都通过程序集的名字(component)确定作用域。
应用程序(例如,application.exe)使用/r编译选项,来引用含有程序集清单的模块。
这样,使得两个模块中的类型都能为程序所用。
示例2.1:
使用CSC.EXE和NMAKE编译多模块程序集
#modulecannotbeloadedasisuntilanassembly
#iscreated
module:
code.cs
csc/t:
modulecode.cs
#typesincomponent.cscanseeinternalandpublicmembers
#andtypesdefinedincode.cs
component.dll:
component.csmodule
csc/t:
library/addmodule:
modulecomponent.cs
#typesinapplication.cscannotseeinternalmembersand
#typesdefinedincode.cs(orcomponent.cs)
application.exe:
application.cscomponent.dll
csc/t:
exe/r:
component.dllapplication.cs
程序集清单存放在一个明确的模块中,并且包含了用于定位类型和资源的所有信息,而这些类型和资源则被定义为程序集的一部分。
图2.4展示了被组合成单个程序集的一组模块,在构建它们时需要相应的CSC.EXE开关。
注意在这个例子中,程序集清单包含对下级模块module和module的文件引用列表。
除了这些文件引用之外,这些下级模块的每个公共类型都通过.classextern指令列出来。
这样,就有了公共类型的完整列表,而不用对程序集中每个模块都遍历元数据。
列表的各项指明了类型所在的文件名,以及唯一标识模块中类型的数值的元数据标记(numericmetadatatoken)。
图2.4:
多模块程序集
最后,含有程序集清单的模块将包含外部引用程序集的主要列表。
列表由程序集中每个模块的依赖关系(dependency)组成,而不仅仅是当前模块的依赖关系。
这样,通过加载单个文件,就能找到程序集所有的依赖关系。
程序集形成一个封装边界(encapsulationboundary),在程序集之间的访问中保护内部实现细节。
程序员可以对类型的成员(例如,字段、方法、构造函数等)实施保护,也可以保护整个类型。
将类型成员标注为internal,将导致它只对同一程序集的模块是可用的。
假如将类型成员标注为public,则导致它对所有代码(当前程序集内部以及外部)是可用的。
假如类型中单独成员(例如,方法、字段、构造函数)还能标注为private,只有该声明类型中的方法和构造函数才能访问。
这样对于组件内部的封装,编程上与传统的C++风格一致。
类似的情形,程序员能将类型成员标注为protected,
它放宽了private所允许的访问限制,使得派生类型的方法和构造函数也能访问该成员。
访问修饰符protected和internal可以组合在一起使用,这样既能够访问当前类型派生的类型,也能够访问同一程序集中的类型。
表2.2展示了特定语言修饰符运用到类型以及单独成员中的情形。
注意在C#中,标注为protectedinternal的成员要么只对同一程序集中的访问方法开放,要么只对派生类型的访问方法开放。
CLR还支持一种访问修饰符(在元数据中的标注为famandassem),既对同一程序集中的访问方法公开,又对派生类型的访问方法公开。
不过,VB.NET和C#并不允许程序员指定这种访问修饰符。
表2.2 访问修饰符
C#
VB.NET
意义
类型
public
Public
访问类型不受限制
internal
Friend
类型只在程序集内部可访问
成员
public
Public*
访问成员不受限制
internal
Friend
成员只在程序集内部可访问
protected
Protected
访问仅限于包含类或者从包含类派生的子类型
protectedinternal
ProtectedFriend
访问仅限于包含类以及从包含类派生的子类型,或者当前程序集的其他类型
Private
Private*
访问仅限于包含类型
*在VB.NET中,通过关键字Dim声明的方法默认为Public,而字段默认为Private。
程序集中定义类型Customer,而在运行时却不发生混淆,但是,这并不能帮助程序员在单个程序中使用两个或多个同名的类型定义。
因为符号化的类型名总是Customer,而不管哪个程序集定义它。
为了解决这种大多数编程语言的限制,CLR类型名会有一个命名空间前缀(namespaceprefix)。
这个前缀是一个字符串,一般以开发人员的组织名(例如,Microsoft、AcmeCorp)开始;如果是.NETFramework的一部分话,则以System开始。
程序集的命名约定通常是基于命名空间前缀。
例如,.NETXML堆栈被部署在System.Xml程序集中,它包含的所有类型都使用System.Xml的命名空间前缀。
这仅仅是一个约定,而不是规则。
例如,类型System.Object存放在名为mscorlib程序集中,而不是名为System的程序集中,尽管也确实存在名为System的程序集。
程序集名字
每个程序集采用四部(four-part)名字,作为唯一的标识。
这四部名字由名称、文化、开发人员以及组件版本构成。
这些名字被存放于程序集自身的程序集清单(assemblymanifest)中,以及引用它的所有程序集的程序集清单中。
在加载时,CLR使用四部程序集名字,找到正确的组件。
CLR提供System.Reflection.AssemblyName类型,便于开发人员对程序集名字进行可编程地访问,具体则是调用该类型的System.Reflection.Assembly.GetName方法。
程序集名字的Name属性往往与程序集清单的底层文件名(不包含任何文件扩展名,尽管它们可能有用)相对应。
这是程序集名字中唯一的不可任选的部分。
对于简单的情形,CLR在加载时只需要Name属性就能够定位正确的组件。
当构建(build)程序集时,名字的这个部分是由编译器根据目标文件名自动选择的。
所有的程序集名字都有一个四部分版本号,其形式为Major.Minor.Build.Revision。
如果你没有显式地设置这个版本号,
表2.3AssemblyVersion特性
特性参数
实际值
1
1.0.0.0
1.2
1.2.0.0
1.2.3
1.2.3.0
1.2.3.4
1.2.3.4
1.2.*
1.2.d.s
1.2.3.*
1.2.3.s
无参数
0.0.0.0
*这里,d是指从2000年2月1日以来的天数,s是从子夜以来的秒数除以2所得的值
默认值将是0.0.0.0。
版本号是在程序构建时设置的,比较典型的方式就是在源代码中使用定制特性。
System.Reflection.AssemblyVersion特性能够接受各种的字符串格式,如表2.3所示。
当你指定版本号时,Major版本号是必须的。
任何缺省部分被假定为零。
在构建时(buildtime),如果Revision部分被指定为*(星号),那么,编译器将利用时钟的时间,为每次编译生成一个单调递增的修订版本号。
如果Build号被指定为*,那么,发射(emit)到程序集清单的Build号,是基于2000年2月1日起的天数,确保每一天程序集都有自己的唯一的构建号。
不过,给定的构建号将只能适用于给定的24小时制。
你不能将版本号的Major或Minor部分指定为*。
稍后,本章将讨论程序集加载器和解析器如何使用程序集的版本。
程序集名字能够包含CultureInfo特性,它能够标识组件开发所用到的语言和国家代码,也就是组件应用的语言环境。
开发人员通过System.Reflection.AssemblyCulture特性指定CultureInfo。
这是Internet工程任务组[InternetEngineeringTaskForce(IETF)]发布的1766号请求注释[RequestforComments(RFC)]所规定的两部分字符串,即字符串的第一部分使用两个小写字母标识语言,第二(可选择)部分使用两个大写字符标识地理区域。
例如,字符串”en-US”标识为美国英语。
含有CultureInfo特性的程序集不能包括代码;准确地说,它们必须是纯资源(resource-only)
的程序集[也称为辅助(satellite)程序集],只能包含区域化的字符串(localizedstring)和用户界面元素。
对于包含代码的单个DLL,辅助程序集允许它们根据被部署的区域有选择地加载(和下载)相应的区域化资源。
包含代码的程序集(也可以说是绝大多数程序集)被认为是文化无关的(culture-neutral),因而也就没有文化标识符。
最后,程序集名字包含一个公钥(publickey),它可以标识组件的开发人员。
一个程序集引用既可以使用完全的128字节的公钥,也可以使用8字节的公钥标记。
公钥(或者公钥标记)被用作处理组织间的文件名冲突。
例如,在内存中和磁盘上可能存在多个utilities.dll组件,它们来自不同的组织,每个组件都被确保拥有唯一的公钥。
下一节我们将更详细地讨论公钥管理。
由于有时必须手工引用程序集(例如,在配置文件中使用),因此,CLR定义了一个标准格式,用于将程序集的四部名字编写为字符串。
这个格式被称为程序集的显示名字(displayname)。
程序集的显示名字总是以程序集的简单Name开始,接着是以逗号为分隔符的属性列表,分别与程序集名字的其它三个属性相对应,并且是可选的。
如果四部名字完全被指定,对应的程序集引用被称为完全限定引用(fullyqualifiedreference)。
如果缺省一个或者更多的属性,则对应的程序集引用被称为部分限定引用(partiallyqualifiedreference)。
图2.5展示了一个显示名字,以及用来控制每个属性的对应的CLR特性。
注意,如果期望程序集没有文化限制,那么,显示名字必须使用Culture=neutral给予标明。
同样,如果期望程序集不带公钥,显示名字也必须通过PublicKeyToken=null标明。
这两种情形与不带Culture或PublicKeyToken属性的显示名字绝然不同。
简单省略显示名字的某些属性,将导致生成一个部分限定名字,它允许匹配任何Culture或PublicKeyToken。
图2.5:
完全限定的程序集名
一般来说,应该避免使用部分限定程序集名字;否则,CLR的许多部分将以非预期的(甚至令人不满意的)方式工作。
然而,为了处理忽略该警告的代码,CLR允许在配置文件中,将部分程序集名字进行完全限定。
例如,考虑下面的应用程序配置文件:
assemblyBinding xmlns: asm="urn: schemas-microsoft-com: asm.v1" > qualifyAssemblypartialName="AcmeCorp.Code" fullName="AcmeCorp.Code,version=1.0.0.0, publicKeyToken=a1690a5ea44bab32,culture=neutral" /> assemblyBinding> 这个配置允许下列对Assembly.Load的调用: Assemblyassm=Assembly.Load("AcmeCorp.Code"); 前面的调用与下面的调用一样: Assemblyassm=Assembly.Load("AcmeCorp.Code,"+ "version=1.0.0.0,publicKeyToken=a1690a5ea44bab32,"+ "culture=neutral"); PartialName特性必须与Assembly.Load的参数完全匹配;也就是说,每个在对Assembly.Load调用中指定的属性,也必须存在于配置文件的PartialName特性中。 并且,在PartialName特性中指定的每个属性,也必须在对Assembly.Load的调用中出现。 稍后,本章将讨论如何定位配置文件。 公钥和程序集 CLR利用公钥技术对组件的开发人员进行唯一标识,同时也保护组件,使它在离开原创者之后不被篡改。 每个程序集有能标识开发人员的公钥,它嵌入在程序集中。 带有公钥的程序集还含有数字签名(digitalsignature),它是在程序集首次发布之前生成的;数字签名还提供了程序集清单的加密哈希值(hash),而程序集清单本身包含了所有附属模块的哈希值。 这样就保证了程序集一旦被发布,程序集中的代码和资源将不再被改变。 数据签名能够只使用公钥进行检验;而签名只能带有对应的私钥(privatekey)才能生成。 由此,组织必须更为谨慎在保护私钥。 现行的CLR版本利用RS
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Net 本质 中文版