CLR Via C 第一章 CLR基础

第一章 CLR执行模型

  .NET框架介绍了许多新的概念和技术。我在这一章的目地是向你介绍关于.NET框架是怎样被设计的一个概述,还有框架中包含的一些新技术,并且定义许多你将会用到的一些条目。我也会介绍把源代码编译成一个应用程序或一系列包含类型(classes,structures)的组件的过程,并且解释你的应用程序是怎样执行的。

1.1将源代码编译成托管模块

  好了,你已经决定使用.NET框架作为你的开发平台。棒极了!你的第一步就是决定你打算生成哪种类型的应用程序或组件。让我们假设你已经完成这个细节;所有事情都已经设计完了,规格也写完了,你已经打算开始你的开发。

  现在你必须决定你要使用哪种语言。这个任务通常很困难,因为不同语言提供了不同的能力。举个例子,在非托管C/C++中,你拥有漂亮的低等级系统控制。你可以精确地管理内存,如果你需要可以很容易的创建线程等等。Microsoft VB6.0,在另一方面,允许你快速地生成UI应用程序并且你可以轻易的控制COM对象和数据库。

  公共语言运行时(CLR)顾名思义:是指可被各种不同编程语言使用的运行时。CLR的核心特性可用于所有面向它的编程语言。

  例如,运行时使用异常报告错误,因此所有面向运行时的语言都可以通过异常获得错误的报告。另一个例子是说运行时允许你创建线程,所以所有面向运行时的语言都可以创建线程。

  实际上,在运行时,CLR并不知道开发人员使用哪个语言完成源代码。这就意味着你无论选择哪种语言来编写代码都会变得很轻松。你可以用任何语言编写代码,只要编译器可以编译面向CLR的代码。

  所以,如果我所说的是真的话,使用一种语言,这种语言和其他语言比起来优势在哪?好吧,我觉得编译器是语法检查器和“正确代码”分析器。它们检查你的源代码,确保你编写的有意义,并且输出描述你意图的代码。不同的语言允许你通过使用不同的的语法开发。不要低估这个选择的价值。举个例子来说,对于数学或者金融类的应用程序,使用APL语言比使用perl语言可以多节省许多的开发时间。

  Microsoft已经推出了多种面向运行时的语言编译器:C++/CLI,C#,Visual Basic,JScript,J#和一个中间语言汇编器(Intermediate Language Assembler)。例外,一些公司,大学也推出面向运行时的代码产品,我知道的有Ada,APL,Caml,COBOL,Eiffel,Forth,Fortran,Haskell,Lexico,LISO,LOGO,Lua,Mercury,ML,Mondrian,Oberon,Pascal,Perl,Php,Prolog,Python,RPG,Scheme,Smalltalk,和Tcl/Tk。

  下图显示了编译源代码文件的过程。从这个图上看出,你可以使用任何一种支持CLR语言编写源代码文件。然后相应的编译器将检查语法,并分析源代码。不管你使用何种编译器,结果都是一个托管模块。托管模块是一个标准的32位Microsoft Windows可移植执行文件(PE32),或者是一个需要CLR去执行的标准64位Microsoft Windows可移植执行文件(PE32+)。

    

  托管模块的组成:

  1. PE32或者PE32+ header:标准的windows PE文件头部,和Common Object File Format(COFF)头部类似。如果这个头部使用PE32格式,这个文件可以在32位或64位版本的windows上运行。如果这个头部使用PE32+格式,这个文件需要在64位版本的windows上运行。这个头部说明了这是一个什么类型的文件:GUI,CUI或者是DLL,并且包含一个说明这个文件是何时创建的时间戳。对于那些只包含IL代码的模块,PE32(+)中的大量信息都会被忽略。对于那些包含本地CPU代码的模块,这个头部还要包含关于本地CPU代码的信息。
  2. CLR header:包含了使其成为托管模块的信息(可以被CLR或其他工具解析)。这个头部包括托管模块所需要的CLR版本号,一些标记,托管模块入口点方法(main方法)的MethodDef元数据标记、以及有关托管模块的元数据(metadata)、资源(resources)、强命名(strong name)、标记(flags)、和其他一些意义不大的信息的位置和大小。
  3. Metadata:每个托管模块包含了元数据表。元数据表主要分两种:一种描述在源代码中定义的类型和成员,另一种描述源代码中引用的类型和成员。
  4. Intermediate Language(IL) code:编译器在编译源代码时产生的代码。在运行时,CLR将IL编译为本地CPU指令

  大多数早期编译器产生的代码都是面向特定CPU架构的,像x86,x64或者IA64。而所有CLR兼容的编译器所生成的都是IL代码。IL代码也被称为托管代码(managed code),因为CLR管理其执行。

  除了生成IL,所有面向CLR的编译器都需要把为每个托管模块生成一个完整的元数据(metadata)。简要的说,元数据(metadata)是一个数据表的集合,这些数据表有一些描述托管模块所定义的内容(如定义的类型和它们的成员),此外还有一些描述托管模块所引用的内容(如引用的类型和它们的成员)。元数据是一些早期技术如类型库(Type Libraries)和接口定义语言(Interface Definition Language)文件的超集。需要指出的是,CLR的元数据比它们还要完整。并且与类型库和IDL不同的是,元数据总是与包含IL代码的文件相关联。事实上,元数据总是和这些代码(IL代码)嵌入到同一个EXE/DLL文件中,两者不可分离。因为编译器同时产生元数据和IL代码并且将它们绑定到生成的托管模块中,元数据和IL代码总能保持同步。

  元数据有很多用途,这里介绍一些:

  • 元数据使得编译时不再需要头文件和库文件,因为所有被引用的类型和成员的信息都已经包含在实现类型和成员的IL文件中。编译器可以直接从托管模块中读取元数据。
  • Visual Studio 使用元数据帮助编写代码。它的智能感知特性通过解析元数据告诉你一个类型所提供的方法、属性、事件和字段,以及方法中的参数。
  • CLR的代码验证过程使用元数据来确保你的代码仅执行“安全”的操作。
  • 元数据允许一个对象的字段序列化到内存块中,传递到另外一台计算机中,然后反序列化,并在远程的计算机上重新创建这个对象的状态。
  • GC使用元数据跟踪对象的生命周期。对于任意一个对象,GC能从元数据获得其类型,以及哪个字段引用了其他对象。

  在第二章中,“生成、打包、部署和管理应用程序及其类型”中,我将会在很多细节中描述元数据。

  C#,VB,JScript,J#和IL编译器总是产生包含托管代码(IL)和托管数据(GC数据类型)的模块。最终用户必须在他们的机器上安装CLR(目前作为.NET Framework的一部分发布)以执行这些包含托管代码或者托管数据的模块。这和为了运行MFC或者VB6.0的应用程序而必须先安装MFC库或者VB DLLs是同一个道理。

  默认情况下,C++编译器生成包含非托管(本地)代码和数据的EXE/DLL模块,在运行时操作的是非托管数据(本地内存)。这些模块并不需要CLR去执行。然而,如果指定/CLR命令行选项,C++编译器就会生成包含托管代码的模块,并且执行环境也必须安装CLR。在所有提到的Microsoft的编译器中,C++是唯一一个允许开发人员编写托管和非托管代码并将其生成在同一个模块中的编译器,它也是Microsoft唯一一个允许开发人员在他们的源代码中同时定义托管代码和非托管代码的编译器。

 

将托管模块组合到程序集

  实际上,CLR并不是和模块一起工作,是和程序集一起工作的。程序集是一个抽象的概念,在开始接触的时候很难掌握它。首先,程序集是一个或多个模块或资源文件的逻辑组合(即程序集由多个模块和资源文件组成)。其次,程序集是复用、安全、版本的最小单元。根据你选择的编译器或工具,你可以生成一个单一文件或者多个文件的程序集。在CLR中,程序集就是我们所说的组件(Component)。

  下图解释了什么是程序集。

  在该图中,使用工具对托管模块和资源(数据)文件进行处理。这个工具生成一个单一的PE32(+)文件来代表文件的逻辑组合这个PE32(+)文件包含一个叫做manifest的数据块。manifest是另一个元数据表的集合。这些表描述了构成这个程序集的文件、由这些文件实现的公有导出类型以及和这个程序集相关的资源或数据文件。

  默认情况下,编译器会将生成的托管模块转换成程序集。也就是说,C#编译器生成一个包含manifest的托管模块,其中的manifest表明程序集仅包含一个文件。所以,对于一个仅包含托管模块并且没有资源(数据)文件的项目来说,这个程序集就是托管模块,你在创建过程中,并不需要任何附加的操作。如果你想要把一组文件组合到一个程序集中,就需要使用更多的工具(assembly linker,AL.exe)及其命令行选项。我将在第二章中解释这些内容。

  对于一个可重用、可部署(securable)、可实施版本管理(versionable)的组件来说,程序集可以将逻辑表示和物理表示相分离(解耦)。如何将代码和资源放置于不同的文件则完全取决于我们自己。

  一个程序集模块同时包含其他被引用的程序集的信息(包括他们版本号)。这个信息使得程序集可以“自己描述”。换句话说,为了执行程序集中的代码,CLR可以确定程序集的直接依赖。不需要再在注册表或活动目录领域服务(Active Directory Domain Services)中获取更多的信息(DLL无需在注册表中注册信息)。因为不需要其他信息,部署程序集比部署非托管组件更简便。

 

加载CLR  

  每个你创建的程序集可以是一个可执行文件或者是一个包含一系列类型的DLL,这些类型供可执行文件使用。CLR负责管理这些程序集中代码的执行。这就意味着在主机上必须安装.NET Framework。
  如果你的机子上已经安装了.NET Framework,那么你就可以在%SystemRoot%\System32目录下找到MSCorEE.dll文件。一台机器上可以同时安装多个.NET Framework版本。使用下面这个路径,可以在注册表中查看安装了哪些版本。
HKEY_LOCAL_MACHINE\SOFAWARE\Microsoft\.NETFramework\policy

 

从.NET Framework2.0版本开始,SDK自带的命令行工具CLRVer.exe可以显示安装的所有CLR的版本。
  在我们讨论CLR加载之前,我们需要花一些时间讨论32位版本和64位版本的windows。如果你的程序集只包含类型安全的托管代码,那么你写的代码在32位和64位的windows上都可以运行,不需要更改任何代码。事实上编译器生成的EXE/DLL文件既可以运行在32位Windows,也可以运行在x64和IA64等64位版本的Windows上。换句话说,一个文件可以在任何安装了相应.NET Framework版本的机器上运行。
  在极少数情况下,开发人员会想要编写在特定版本的windows上运行的代码。通常是在使用unsafe code或需要与面向特定CPU架构的非托管代码进行互操作时。为了帮助这些开发人员,C#编译器提供了/platform命令行选项。这个选项允许你指定这个结果程序集是要运行在x86机器(32位windows)还是在x64(64位windows)上,或Intel Itanium机器(64位Windows)。如果没有指定平台,默认情况为anycpu,说明这个结果程序集可以运行在任何版本的windows上。isual Studio用户可以在项目属性页中设置项目所面向的平台。
  根据这个平台选项,C#编译器会生成包含PE32或PE32+头部,同时还包括所需的CPU架构。Microsoft发布了两个命令行工具,DumpBin.exe和CorFlags.exe,你可以使用它们来查看编译器生成的托管模块的头部信息。
  运行一个可执行文件时,windows检查这个EXE文件头部,决定这个应用程序是需要32位还是64位的地址空间。以PE32开头的文件可以在32位或64位地址空间上运行,而以PE32+开头的文件需要64位的地址空间。windows同时检查嵌入到头部的CPU架构信息以确保它可以匹配在这台机子上的CPU类型。目前,64位版本的windows提供一种可以运行32位应用程序的技术,叫做WoW64(for windows on windows64).它还能模仿x86指令集,允许基于x86本地代码的32位应用运行在Itanium机器上,尽管性能有很大损失。
  下表显示两个内同。第一,当你指定不同的/platform命令行选项时你将得到什么类型的托管代码。第二应用程序是怎样在不同版本的windows上运行的。
  在windows检查EXE文件头部以决定是创建32位,64位还是WoW64进程后,windows将x86,x64或者IA64版本的MSCorEE.dll加载到进程的地址空间中。在x86版本的Windows中,x86版本的MSCorEE.dll位于C:\Windows\System32目录下。x64或IA64版本的Windows中,x86版本的MSCorEE.dll位于C:\Windows\SysWow64目录,64位版本的位于C:\Windows\System32目录(为了向后兼容)。然后,进程的主线程调用MSCorEE.dll中定义的方法。该方法初始化CLR,加载EXE程序集,然后调用它的入口点方法(Main)。这样托管的应用程序就建立并运行了。(可以在代码中使用Environment的Is64BitOperatingSystem或Is64BitProcess属性来检查是否运行在64位版本的Windows或运行在64位的地址空间中)。
  如果一个非托管应用程序调用LoadLibrary方法去加载托管程序集,windows将加载并初始化CLR(如果还未加载的话)。当然在该场景中,进程已经创建并运行,这将限制程序集的可用性。如,使用/platform:x86编译的托管程序集不能被64位的进程加载,反之,一个使用同样的选项编译的可执行文件将以WoW64的形式被64位版本的Windows加载。

执行程序集代码

  正如前面谈到的,托管程序集包括元数据和IL。IL是一种独立于CPU的机器语言。IL比大多数的CPU机器语言更高级。IL可以访问和操作对象类型,可以创建和初始化对象,调用对象的虚方法和直接操作数组元素。它甚至抛出和捕获异常。你可以把IL想象成面向对象的机器语言
  通常,开发人员会使用高级语言编写代码,像C#,C++/CLI,VB等等。这些高级语言的编译器都会生成IL。然而,正如任何其他机器语言,IL可以用汇编语言编写,微软也确实提供了IL汇编器,ILAsm.exe。微软还提供了IL反汇编,ILDasm.exe。 
  任何高级语言最多只能使用CLR全部特性一个子集,而IL汇编语言允许开发人员获取CLR的所有特性。因此,如果你真的想使用被你的编程语言隐藏掉的CLR特性,你可以选择使用IL汇编或提供了这部分功能的编程语言来编写这部分代码
  在执行一个方法的时候,它的IL首先要转化成当地的CPU指令。这是CLR中(JIT--just-in-time)编译器的工作。
  下图说明了一个方法第一次被调用时的情况
  Main方法执行前,CLR检查所有被Main方法引用的类型。这将导致CLR分配一个内部的数据结构,这个数据结构用来管理这些被引用的类型。在上图中,Main方法只引用了一个类型,Console,所以CLR分配了一个单一的内部数据机构。这个内部数据结构包含在Console类型中定义的每个方法的入口。每个入口都有一个地址,通过这个地址可以找到方法的实现部分。当初始化这个结构时,CLR把每个入口设置成内部的一个没有正式记录的函数,我们暂且成该阐述为JITCompiler
  (1)当Main第一次调用WriteLine时,JITCompiler函数也被调用JITCompiler负责吧一个方法的IL代码编译成本地CPU指令。因为IL是被“即时(just in time)”编译的,所以CLR的这一部分通常被称作JITter或者JIT编译器。
  当JITCompiler函数被调用时,它知道哪个方法被调用,在这个方法中定义了什么类型。(2)JITCompiler函数在程序集的元数据中查找被调方法的IL代码。JITCompiler验证IL代码并将其编译编译成本地CPU指令。本地CPU指令保存在一个动态分配的内存块中。然后(3)JITCompiler将前面内部数据结构中被调用方法的地址替换成包含本地CPU指令的内存块地址(4)最后JITCompiler函数会跳转到内存块中的代码。这里的代码就是WriteLine方法的实现(含有一个String类型参数的版本)。(5)当这些代码执行完,它将返回到Main函数中,Main函数会接着继续执行下面的代码
  现在Main第二次调用WriteLine方法。这一次,WriteLine的代码已经经过验证和编译。所以这次将直接调用到内存快,完全跳过了JITCompile函数。WriteLine执行完后,同样返回到Main
  下图说明了第二次调用WriteLine方法时的过程。
  这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。
  JIT编译器将本地存储在动态内存之中。这以为这当应用程序关闭时,编译生成的本地代码将被丢弃。这样,如果我们以后再次运行该应用程序,或者同时运行该应用程序的两个不同实例,JIT编译器需要再次将同样的IL代码编译成本地指令
  对于大多数的应用程序,JIT编译引起的损失是微不足道的。而且,大部分的也能够用程序经常反复调用同一个方法。这样,在应用程序执行时,这些方法引起的也只是一次性能损失。而且通常方法内部执行所花费的时间要比方法调用本身索花费的时间要多的多。
  CLR的JIT编译器对本地代码的优化方式与非托管的C++编译器后端所做的工作类似。生成优化代码可能要花费很多时间,但是代码的性能要明显优于未优化的。
  C#编译器有两个影响代码优化的选项:/optimize和/debug。
  当生成非优化代码时,C#编译器会在代码中生成NOP(no-operation,非操作)指令。NOP指令可以在调试的时候使用“编辑后继续运行(edit-and-continue)”的特性,同时也允许在控制流指令(如for、while、do、if、else、try、catch和finally)中设置断点,以易于调试。在生成优化的IL代码时,C#编译器会移除这些NOP指令。
  当JIT编译器生成优化代码时,控制流也会被优化,这将导致这些代码在调试的时候是很难单步运行的,并且有些在调试器内部执行的功能也无法工作了。
  当你在Visual Studio创建一个新的C项目,该项目的调试(Debug)配置是/optimize-和/debug:full选项,发布(Release)的配置/optimize+/debug:pdbonly
  

IL与验证

   IL是基于堆栈的,这就意味着它的所有指令将操作数压入执行栈中,并且从栈中弹出结果。因为IL没有提供操作寄存器的指令,编译器开发者可以很容易地生成IL代码。
  IL指令是无类型的。举个例子来说,IL将最后两个操作数压入栈中以执行相加的指令,是没有32位和64位指令的区分。
当执行这个相加指令时,它首先判断堆栈上操作数的类型,然后进行适当的操作。
  在我看来,IL的最大好处不是对底层CPU的抽象,而是它提高了程序的健壮性和安全性当IL被编译成本地CPU指令的时候,CLR会执行“验证”过程。验证过程检查高级IL代码,确保它做的每件事情都是“安全”的。例如,验证过程检查每个被调用的方法参数数量是否正确,并且每个参数的类型要正确匹配;每个方法的返回值都必须被正确的使用;每个方法都必须有一个返回语句等等。托管模块的元数据包含了所有验证过程需要的方法和类型信息。
  在windows中,每个进程都拥有它自己的虚拟地址空间。分离地址空间是非常必要的,因为你不能信任程序的代码。应用程序完全可能(不幸的是,这种情况很经常发生)读写一个无效的内存地址。将每个windows进程放在独立的地址空间提高应用程序的健壮性,因为这样一个进程就不会干扰另一个进程的运行。
  通过验证托管代码,你可以确保他们不会访问它们不应该访问它们不应该访问的内存,一次也就不会干扰另一个应用程序的代码。这意味着我们可以在一个单独的window虚拟地址空间内运行多个托管应用程序。
  因为window进程需要很多操作系统资源,太多的进程会损伤性能,并限制系统中可用的资源。在一个独立的操作系统进程中运行多个应用程序可以减少进程数量,可以改善性能、减少对资源的使用、并且不会影响健壮性。这是托管代码相比非托管代码的又一个优势所在。
  实际上,CLR提供了在单个操作系统进程中运行多个托管应用程序的能力。每个托管程序被称作“应用程序域(AppDomain)”。默认情况下,每个托管EXE文件只在它自己的地址空间上运行,这个地址空间上只有一个应用程序域。但是,CLR的宿主进程(像IIS或SQL Server 2005)可以决定在一个单独的操作系统进程中运行多个应用程序域。我会在第22"CLR宿主及应用程序域"中做详细讲解。

不安全的代码

  Microsoft C#编译器默认会生成安全代码。“安全代码”是经过验证的代码。但是Microsoft C#编译器也允许开发人员编写非安全代码。非安全代码可以直接在内存地址上工作,并且在这些地址上操作字节(bytes)。这是一个非常强大的功能,当你想要和非托管代码进行互操作或者想要提高一个对时间要求比较高的算法性能时,你可以使用非安全代码。
  但是,使用非安全代码会带来一个重大的风险:非安全代码会破坏数据结构,甚至可以利用它打开安全漏洞。因为这个,C#编译器需要所有包含非安全代码的方法使用unsafe关键字。例外,C#编译器需要你使用/unsafe选项来编译代码。
  当JIT编译器尝试编译一个非安全代码时,它回去检查这个程序集包含的方法是否设置了System.Security.Permissions.SecurityPermission中的System.Security.Permissions.SecurityPermissionFlag SkipVerification 标志。如果设置了这个标志,JIT编译器就会编译非安全代码并且允许它执行。CLR会信任这些代码并希望直接访问地址和字节操作不会有什么损伤。如果没有设置这个标志的话,JIT编译器会抛出System.InvalidProgramException 或System.Security.VerificationException异常,阻止方法的运行。事实上,整个应用程序都有可能在这个点终止,至少不会有什么伤害。
  默认情况下,对于安装在用户电脑上的程序集是完全信任的,它们可以做任何事情,包括执行非安全代码。但是,对于从通过因特网执行的程序集不能执行不安全代码。如果它包含不安全代码,就会抛出前面提到的异常。一个管理员/最终用户能够改变这种默认设置;但是,管理员对这段代码的行为负全部责任。
  微软提供了一个叫做PEVerify.exe的工具,它会监测程序集里所有的方法,并提示你哪些方法中含有不安全代码。这也让你知道通过企业内部网(Intranet)或因特网执行你的程序可能会出现哪些错误。
  验证过程需要访问任何独立的程序集中的元数据。所以当你使用PEVerify.exe去检查程序集时,你必须能够找到并加载所有程序集的引用。由于PEVerify 是使用CLR来定位程序集的,所以当执行这个程序集的时候,我们会使用相同的binding和探测规则来将程序集定位。我会在第二章和第三章“共享程序集和强命名程序集”中来介绍这些bindingprobing rules
  题外话:
  IL和保护你的知识产权
  一些人可能会想IL并没有对他们的算法提供足够的知识产权保护方法。换句话说,他们会想我生成了一个托管模块,别人可能会使用像IL Disassembler之类的工具,很容易的对你的程序代码进行反向工程。
  确实,IL代码比其他大多数的汇编语言更高级,对IL代码进行反向工程也越容易。但是,当实现服务端代码(像Web 服务,Web窗体或存储过程)时,程序集是在你的服务端的。所以在外面的人不可以访问你的程序集,也不可能使用工具查看IL——你的知识产权是安全的。
  如果你关心你发布的程序集,你可以使用第三方混淆工具来保护代码。这些工具可以混淆程序集中元数据的私有符号,使得难以还原这些名称,也就难以理解代码的意图。注意这其实只能起到很小的保护作用,因为IL必须在一些点有用,CLR需要这些点对IL进行JIT编译
   如果你不觉得混淆器可以保护你的知识产权,你可以使用非托管模块来实现你的敏感算法,这些非托管模块使用本地CPU指令代替IL和元数据。然后使用CLR的互动性(interoperability)特性(假设有充分的许可)在非托管与托管代码之间进行通信。
  

本地代码生成器NGen.exe

  NGen.exe工具是.NET Framework的一部分,当在用户的电脑上安装应用程序时,它可以用来将IL代码编译成本地CPU指令因为代码是在安装的时候编译的,所有CLR的JIT编译器不需要在运行的时候编译IL代码,这样可以提高应用程序的性能。下面是NGen.exe使用的场景
  • 缩短应用程序的启动时间      运行NGen.exe可以缩短启动时间,因为代码已经被编译成本地CPU指令,在运行的时候不需要再编译。
  • 减少应用程序的工作区大小   你可能知道一个程序集可以同时被多个进程/应用程序域加载,在这个程序集中使用NGen.exe能够减少应用程序的工作区。因为NGen.exe可以将IL编译成本地CPU指令,并将输出保存到一个独立的文件中。这个文件可以通过内存映射到多进程的地址空间,并允许代码共享;每个进程不需要单独的拷贝。
当安装程序对应用程序或单独的程序集调用NGen.exe时,应用程序里的所有程序集或者那个指定的程序集都会将IL编译成
本地代码。NGen.exe会生成一个只包含本地代码(不包括IL代码)的新程序集文件。这个新文件会被放到一个文件名类似于“C:\windows\Assembly\NativeImages_v2.0.50727_32”的目录下。这个目录名包含CLR的版本号,以及本地代码编译为x86、x64还是Itanium。
  当CLR加载程序集文件时,CLR会去检查是否存在相应的NGen后的本地文件。如果文件未找到,CLR的JIT编译器会像平常那样编译IL代码。如果文件找到了,CLR会使用本地文件中编译过的代码,而且文件中的方法在运行时不会再编译。
  表面上,这听起来不错。你好像可以获得托管代码所有的好处,又避免了性能问题(JIT编译),似乎很棒。但其实有很多潜在问题:
  • 没有代码保护  许多人可能觉得可以不用发布IL代码文件而只发布NGen后的文件,这样就可以保护他们的代码。不幸的是,这是不可能的,在程序运行的时候,CLR需要访问程序集的元数据;这就需要发布包含IL和元数据的程序集。另外,如果出于一些原因,CLR不能使用NGen.exe编译后的文件,那么CLR就需要JIT编译器编译程序集的IL代码,这是IL就必须是可用的。
  • NGen的文件可能会不同步  当CLR加载NGen的文件时,会比较前期编译的代码和当前执行环境的诸多特征。如果其中有一个特征不符合,NGen的文件就不能使用,取而代之的仍然是普通的JIT编译过程。必须匹配的特征如下:
1.程序集模块版本ID(Assembly module version ID,MVID)
    2.引用的程序集版本ID
    3.CLR版本
    4.生成类型(release,debug,optimized debug,profiling等等)
  • 执行时性能差  编译代码的时候NGen不能像JIT编译器那样对执行环境做很多假设,这也导致NGen 生产低效的代码。
对于服务器端应用程序,NGen几乎没有意义,因为只有第一次请求会损害性能,以后的客户端请求能够全速运行。而且对于服务器应用,只需要一个代码实例。
  对于客户端应用程序,NGen会缩短启动时间,也可以在一个程序集同时被多个应用程序使用时减小工作区。即使程序集没有用于多个程序,NGen仍然能够改善工作组。此外,如果所有客户端程序都使用NGen,CLR就没有必要加载JIT编译器,这能进一步减少工作组。当然只要一个程序集没有使用NGen或者不能使用NGen,JIT编译器都将加载,并增加工作组。
  

框架类库

  .NET Framework包含了框架类库(Framework Class Library,FCL)。FCL是一系列DLL程序集,它包含几千个类型定义,每种类型都提供了一些功能。Microsoft也提供了一些额外的类库,像WinFX,DirectX SDK。这些额外的类库提供更多的类型和功能供你使用。
  下面是一些允许开发人员使用的程序集,可以用来创建各种应用程序:
  • Web services  这个方法可以使得将基于XML的信息放送到Internet的操作变得很容易。
  • Web Froms    基于HTML的应用程序(Web sites)。通常情况下,Web窗体应用程序一些数据库查询和web服务调用,并对返回的信息进行组合和筛选,最后使用一个基于rich-HTML的用户界面将信息表示在浏览器中。
  •  Windows Forms  富windows GUI应用程序。你可以使用windows桌面提供的更强大的,更高级的函数创建你的应用程序的UI来代替Web窗体。Windows窗体可以使用控件、菜单、鼠标和键盘事件,甚至可以直接和底层操作系统交互信息。和Web窗体应用程序一样,Windows窗体一样也可以进行数据库查询和调用Web services。
  • Windows console applications  一个对UI要求不高的应用程序,控制台应用程序提供一种快速、容易的方法来创建应用程序。各种编译器、实用程序、工具通常被实现为控制台应用程序。
  • Windows services  利用.NET Framework,我们可以生成由Windows服务控制管理器(Windows Service Control Manager,SCM)控制的服务程序。
  • Component library  .NET Framework允许你生成独立的程序集(组件),它们可以应用于前面提到的各种应用程序。
FCL包含数以千计的类型,一系列相关的类型放在一个命名空间(namespace)中提高给开发人员。例如,System命名空间就包含了基类型Object,所有其他的类型都直接或者间接由此继承来的。例外,System命名空间还包含了整数、字符、字符串、异常处理、控制台I/O,以及许多实用类型,它们可以用来安全的转换数据类型,格式化数据类型、产生随机数以及执行各种数学运算。所有的应用程序都会用到System命名空间中的类型。
  为了使用.NET平台的各种特性,你应该知道自己需要的类型包含在哪个命名空间中。许多类型允许你自己定制它们的行为;你只需要从FCL类型中继承你想要的类型。面向对象的.NET框架为软件开发人员提供的一个一致的编程模式。同时,开发人员可以很容易的创建包含他们自己类型的命名空间。这些命名空间和类型可以和面向对象的编程模式无缝的结合。相对于Win32编程范式,这种新方法大大的简化了软件开发。
  FCL中大多数命名空间所提供的类型可以用于任何类型的应用程序。下图列出了一些通用的命名空间,并且简要的进行了描述。
  这本书是关于CLR和一些与CLR紧密关联的常规类型。所以这本书里面的内容可以应用于所有面向CLR的应用程序和组件。很多其他的好书会讲解一些特定的应用程序,比如web服务,web窗体,windows窗体等等。这些特定应用程序的书会帮助大家从高层的角度来学习.NET应用程序开发,因为他们关注的是应用程序的种类,而非底层开发平台。在这本书里,我会教你许多底层的东西,读完本书之后,大家可以再读一些特定的应用程序相关的书籍,便可以轻松熟练的创建各种.NET框架应用程序了。
  

通用类型系统

  到目前为止,你应该已经知道CLR是关于类型的。类型为应用程序和组件提供了它们所需的功能。类型提供了一种机制,这种机制可以使得一种语言编写的代码可以和另一种语言编写的代码进行交互。由于类型是CLR的基础,微软创建了一个正式的规范--通用类型系统(Common Type System,CTS)用来描述类型的定义和行为。
  CTS规范规定一个类型可以包含零个或更多的成员。本书的第Ⅱ部分,我将会对所有这些成员做详细的介绍。目前大家只需要对它们有一个简单的了解。
  • Field  数据变量,对象状态的一部分。字段通常由它们的名字和类型来识别的。
  • Method  在对象上执行某种操作的函数,可以改变对象的状态。方法有一个名字、签名和修饰符。签名指定参数的数量、系列和类型,是否有返回值以及返回值的类型等等
  • Property  对于调用者,属性就像是一个字段。但对于属性的实现者,属性看起来就像是一个方法。属性允许实现者在访问数值之前验证输入参数和对象状态,或者在必要的时候只计算数值。属性的调用者只需要很简单的句子就可以完成属性的调用。最后属性允许你创建只读或只写“字段”。
  • Event  事件允许在一个对象和其他相关联的对象之间建立一个通知机制。例如,一个按钮可以提供一个事件,当按钮被按下时,将通知其他对象。
CTS同时也定义了对象可见性和访问类型成员的规则。例如,将一个类型修饰为public,将是的它对于任何程序集都是可见的。另一方面,将一个类型定义为assembly(在C#里叫做internal),将是的它仅对于其所定义的程序集中的代码可见。CTS建立了以程序集为类型可见性边界的规则,CLR实现了这种可见性规则。
   类型对调用者是否可见限制了调用者访问这个类型成员的能力。下面列举了一些关键字:
  • Private  方法只能被同一个类型中的其他方法所调用。
  • Family  方法可以被子类调用,不管他们是不是在同一个程序集。C++和C#用protected代替family。
  • Familyassembly  方法可以被子类调用,但是子类只能在同一个程序集中实现。C#用protected internal代替family或者assembly。
  • Public  方法可以被任何程序集的代码调用。      
  另外,CTS定义了管理类型的继承,虚方法,对象生命周期之类的规则。设计这些规则的目的在于使它们的语义可以用现代编程语言方便的表达出来。事实上,你甚至可以不用去学习CTS规则,因为你选择的语言已经提供了我们所熟悉的语言语法和类型规则,并且在生成托管模块时会将这些特定语言的语法映射为IL 
  当我第一次使用CLR的时候,我意识到把语言和代码行为分成两个部分是最好的。使用C++,你可以定义自己的类型和成员,使用C#或者VB也可以定义相同的类型和成员。当然,语法是不同的,但是类型的行为是完全相同的,因为CLR的CTS定义了类型的行为。
  为了解释这个思想,我们举个例子吧。CTS规定一个类型只能从一个基类继承。尽管C++语言支持多继承,但是CTS不会接受运行这样的代码。为了帮助开发人员,Microsoft的C++/CLI编译器会在出现多继承的时候报错。
  这里有一些CTS的其他规则。所有类型都必须继承于一个预定义类型:System.Object。正如你知道的Object是在System命名空间中定义的。Object作为其他类型的根类可以保证每个类型实例拥有一个最小的行为集。具体而言,System.Object允许你做以下的事情:
  • 判断两个实例是否相等。(Equals)
  • 获得实例的hash码。(GetHashCode)
  • 查询实例的真正类型。(GetType)
  • 实现实例的浅拷贝。()
  • 获得实例当前状态的字符串表示。(ToString)

通用语言规范

  COM允许不同语言创建的对象能够进行相互之间的访问。另一方面,CLR现在集成所有的语言,允许使用一种语言创建的对象在另一个不同语言编写的代码中被看做同等的成员。CLR标准的类型集合,元数据(自身描述类型信息)和通用运行环境使得这种集成成为可能。
  语言集成是一个很遥远的目标,编程语言之间有很大的区别。举个例子来说,一些语言不区分大小写符号,有些不提供无符号整数,有些不支持操作符重载,有些方法不支持可变数目的参数。
  如果你希望你创建的类型可以被其他编程语言方便的访问,只能使用编程语言中那些对其他语言来说也可用的特性。为了解决这一问题,微软定义了一个通用语言规范(Common Language Specification,CLS),该规范为编译器厂商详细描述了面向CLR的编译器必须支持的一个最小特性集合
  CLR/CTS支持的功能比CLS定义的自己多的多,如果你不关心语言之间的可操作性,你可以开发一套功能非常丰富的类型,它们仅受你使用的那种语言的功能集的限制。具体的说,在开发类型和方法的时候,如果希望它们对外“可见”,能够从符合CLS的任何一种编程语言中访问,就必须遵守由CLS定义的规则。注意,假如代码只是从定义程序集的内部访问,CLS规则就不适用了。下图形象地演示了这一段想要表达的意思。
  在图中,CLR/CTS提供了一系列特性。一些语言提供了CLR/CTS的较大子集。举个例子,开发人员使用IL汇编语言可以使用所有CLR/CTS提供的特性。而大多数语言,如C#、VB、Fortran,提供了CLR/CTS特性的一个子集。CLS定义了所有语言必须支持的一个最小的特性集合。
  如果你打算采用某种语言设计一个类型,并希望该类型被其他语言使用,那么就不能在public和protected成员中使用任何CLS以外的特性。否则意味着使用其他语言的程序员可能无法访问你的类型成员。
  下面的代码用C#定义一个CLS兼容的类型。其中一些与CLS不兼容的构造会导致C#编译器报错。
using System;

// Tell compiler to check for CLS compliance
[assembly: CLSCompliant(true)]

namespace SomeLibrary {
// Warnings appear because the class is public
public sealed class SomeLibraryType {

// Warning: Return type of 'SomeLibrary.SomeLibraryType.Abc()'
// is not CLS-compliant
public UInt32 Abc() { return 0; }

// Warning: Identifier 'SomeLibrary.SomeLibraryType.abc()'
// differing only in case is not CLS-compliant
public void abc() { }

// No warning: Method is private
private UInt32 ABC() { return 0; }
}
}

 

在这段代码中,我们在程序集上应用了[assembly:CLSCompliant(true)]特性。这个特性告诉编译器确保任何一个公共暴露的类型中不会有阻止其他编程语言访问该类型的构造。当代码编译时,C#编译器会发出两个警告。第一个警告是因为Abc方法返回一个无符号整数,有些编程语言不能操作无符号整数值。第二个警告是因为该类型暴露的两个公有方法abcAbc仅存在大小写和返回值的差别,VB等语言不能调用这两个方法。
  有趣的是,如果你删除sealed class SomeLibraryType之前的public 再重新编译,两个警告都会消失。这是因为SomeLibraryType的类型默认为internal,不会暴露给外部程序集。
  让我们来简化一下CLS的规则。一个类型的每个成员不是字段(数据)就是方法(行为)。这意味着每个编程语言都要能够访问数据和调用方法。确报字段和方法可以通过特殊或者通用的方式来使用。为了简化编程,语言通常提供了额外的抽象,对这些常见的编程模式进行简化。例如,语言会公开枚举、数组、属性、索引、委托、事件、构造器、析构器、操作符重载、转换操作符等概念。编译器在源代码中遇到上述任何一种东西时,必须将其转换成字段和方法,使CLR和其他任何编程语言能够访问这些构造。
  下面的类型包含了构造函数、析构器、操作符重载、属性、索引器和一个事件。注意这里的代码仅仅为了通过编译,并不是一个类型正确的实现方法。
using System;

internal sealed class Test {
// Constructor
public Test() { }

// Finalizer
~Test() { }

// Operator overload
public static Boolean operator ==(Test t1, Test t2) {
return true;
}
public static Boolean operator !=(Test t1, Test t2) {
return false;
}

// An operator overload
public static Test operator +(Test t1, Test t2) { return null; }

// A property
public String AProperty {
get { return null; }
set { }
}

// An indexer
public String this[Int32 x] {
get { return null; }
set { }
}

// An event
public event EventHandler AnEvent;
}

 

当编译器编译这段代码的时候会得到一个类型,其中含有大量字段和方法。可以使用.NET框架SDK配套的IL反编译工具(ILDasm.exe)来检查最终生成的托管模块,如图

  (我在这里使用的另一个强大的反编译软件reflector.exe
  下表1-4 解释了编程语言构造是如何映射到CLR字段/方法
  Test类型还有另一些节点未在表1-4中列出,其中包括.CLASS.customAnEventAProperty以及Item——它们标识了与类型有关的其他元数据。这些节点不映射到字段或方法;它们只是提供了有关类型的一些额外的信息,供CLR、编程语言或者工具访问。例如,利用Reflector工具,可以发现Test 类型提供了一个名为AnEvent的事件,该事件通过两个方法(add_AnEvent 和 remove_AnEvent)公开。

与非托管代码互操作

  .NET Framework提供了许多其他开发平台没有的功能。但是,很少有公司愿意重新设计和实现他们的代码。Microsoft意识到这一点,并构建出了CLR,CLR允许一个程序同时包含托管代码和非托管代码。具体的说,CLR支持三种互操作情形:
  • 托管代码可以调用DLL中的非托管函数  托管代码可以用一种名为P/Invoke(Platform Invoke的简称)的机制来轻松调用DLL中包含的函数。总之,FCL中定义的许多类型都要在内部调用从Kernel32.dll、User32.dll等导出的函数。许多编程语言都提供了一个机制,允许托管代码方便地调用包含在DLL中的非托管函数。例如,C#应用程序可调用从Kernel32.dll 导出的CreateSemaphore函数。
  • 托管代码可以使用现有的COM组件(服务器)  许多公司已经实现了许多非托管的COM组件。使用这些组件的类型库,可以创建一个托管程序集来描述COM组件。托管代码可以访问像访问其他任何托管类型一样访问托管这个程序集中的类型。有关这方面的信息,你可以.NET Framework SDK配套提供的TlbImp.exe工具。有的时候,你可能没有一个类型库,或者想对TlbImp.exe生成的内容进行更多的控制。在这种情况下,可以在源代码中手动构建一个类型,使CLR能用它来实现正确的互操作性。例如,可以从一个C#应用程序中使用DirectX COM组件。
  • 非托管代码可以使用托管类型(服务器)  许多现有的非托管代码需要你提供一个COM组件以保证代码可以正确地工作。许多现有的非托管代码要求提供一个COM组件,从而确保代码正确工作。使用托管代码可以更简单地实现这些组件,避免所有代码都不得不和引用计数以及接口打交道。例如,可以使用C#来创建一个ActiveX控件或者一个shell扩展。这方面的详情可以参见.NET Framework SDK配套提供的TlbExp.exe和RegAsm.exe工具。
posted @ 2010-09-22 16:02  wl98766789  阅读(1613)  评论(0)    收藏  举报