微软.NET Framework介绍了不少概念、技术和术语。在这一章个人目标是给你一个概述,.NET Framework是怎么设计的,介绍一些框架包含的技术,和不少定义的术语,当你开始使用.NET Framework的时候将会看到这些。我也将经过带你创建你本身的源码应用程序或者一个可再使用组件(文件集)集合包含(类,枚举,等等)向你解释一个应用程序是将怎么执行。 程序员
Compiling Source Code into Managed Modules(编译源码到托管模块) 算法
那么你已经决定使用.NET Framework做为你的开发平台了。你的第一步是决定你想要开发的是哪种类型的应用程序或者组件。让咱们来假定你已经完成了这个小细节;每一件事都已经设计好了,规格说明书都已近写好了,而且你已经准备开发了。 数据库
如今你必须决定要使用哪种开发语言。通常而言,这个任务有点难度,由于不一样的语言拥有不一样的能力。好比,非托管的C/C++,你能控制底层的系统。你能经过你想要的方式精确的管理内存,当你想建立线程的时候很容易,等等。微软Visual Basic 6.0,在另外一方面,容许你快速的建立UI应用程序和可让你容易的控制COM组件和数据库。 编程
公共语言运行库(CLR)正如它的名字同样:runtime可用于不一样的和各类各样的编程语言。CLR的核心特征(好比内存管理,程序集加载,安全,异常处理,和线程同步)可适用于任何和全部编程语言只要编译目标期间是CLR。好比,runtime使用异常报告错误,因此全部编译目标是runtime的编程语言得到错误报告都是经过异常获得的。另外一个例子是runtime容许你建立线程,因此全部编译目标是runtime的编程语言均可以建立线程。 后端
事实上,在runtime库中,CLR不知道开发者使用哪种开发语言写的源码。这意味着你选择的开发语言应该是最容易表达你的意图的。你能够用任何你想用的开发语言只要你使用的编译器能把你的代码编程成CLR。 数组
因此,假如我说的真的,使用某个开发语言而不使用另外一个开发语言有什么好处?好吧,我认为编译器做为语法检查者和"代码纠错"分析者。它们检查你的源代码,确保你写的源码有一些道理,而后输出描述你意图的代码。不一样的编程语言容许你在开发时使用不一样的语法。不要低估选择开发语言的价值。好比,对于数学或者财政应用程序,使用APL语法表达你的开发意图能够节省数日开发时间相较于使用Perl语法表达相同的开发意图。 浏览器
微软已经建立了几门语言编译器编译成runtime:C++、CLI,C#(发音"C sharp"),Visual Basic,F#(发音"F sharp"),Iron Python,Iron Ruby,和IL汇编。除微软外,另外几家公司、大学都建立了编译器而且产生的代码目标是CLR。我知道的编译器可编译的语言有Ada,APL,Caml,COBOL,Eiffel,Forth,Fortran,Haskell,Lexico,LISP,LOGO,Lua,Mercury,ML,Mondrian,Oberon,Pascal,Perl,PHP,Prolog,RPG,Scheme,SmallTalk,和 Tcl/Tk。 sass
下图指出了程序编译源码文件。如图所示,你可使用任何支持CLR的编程语言建立源码文件集。而后你可使用相应的编译器检查语法和分析源码。无论你使用哪个编译器,结果都是托管组件。一个托管模块是一个标准的32位Windows PE32文件或者标准64位Windows PE32+文件只能在CLR执行。顺便说一句,在Windows中托管程序集会获得DEP和ASLR的好处,这两个特征提升了你整个系统的安全性。 安全
图表描述了托管模块的组件。 服务器
托管组件组成部分
组件名称 |
描述 |
PE32或者PE32+ header |
标准Windows PE文件头,它和COFF(Common Object File Format)的头文件很像。假如头文件使用PE32转换,那么所转换文件可在32位或者64位的Windows系统上运行。假如头文件使用PE32+转换,那么文件只能在64位版本的Windows系统上运行。头文件也规定了文件的格式:GUI,CUI,或者DLL,而且包含一个文件什么时候建立的时间戳。对于只包含IL代码的模块,大部分在PE32(+)头文件中的信息都会被忽略。对于包含本地CPU代码的模块,这个头文件包含了本地CPU代码信息。 |
CLR header |
包含标记当前模块为托管模块的信息(经过CLR和工具解释得来的信息)。头文件包含所需的CLR版本,一些标记,托管模块方法(Main method)入口点的MethodDef元数据令牌。模块元数据的位置和大小,资源,强命名,一些标记,和其余少许有趣的东西。 |
元数据 |
每一个托管模块都包含元数据表单。全部的表单中有两个主要的分类:一种是描述你源码中定义的类型和成员,另外一种是描述源码中的被引用的类型和成员。 |
IL Coder |
编译器编译产生的代码。在运行时,CLR把IL编译成本地CPU指令。 |
本地代码编译器按照指定的CPU架构生成代码,好比x86,x64,或者ARM。全部顺从CLR编译器的都会生成IL。(我在后面的章节会深刻更多的IL代码细节。)IL代码有时会被归为托管代码,由于CLR管理它的执行。
除了生成IL,每个以CLR为编译目标的编译器都要求在每一个托管模块中生成全部元数据。简而言之,元数据是数据表的一个集合,描述了在模块中定义了什么,好比类型和成员。此外,元数据也有表指出托管模块的引用,好比导入的类型和导入的成员。元数据是老技术的一个超集,好比COM's Type Libraries和Interface Definition Language(IDL) 文件。重点须要注意的是CLR元数据更完整。而且,不一样于Type Libraries和IDL,元数据一直是和包含IL代码的文件是关联的。事实上,元数据一直做为代码嵌入到相同名字的EXE/DLL中,使具备相同名字的EXE/DLL不能分离。由于编译器生成元数据和代码的同时把它们绑定到托管模块,元数据和IL代码不能分开描述。
下面是元数据的一些用处:
*编译时元数据移除了本地的C/C++头文件和库文件,由于全部类型/成员引用信息已经包含在IL中,IL实现了类型/成员。编译器能够直接从托管模块中读取元数据。
*微软Visual Studio使用元数据帮助你写代码。它的智能提示特性经过转换元数据告诉你方法须要的属性,事件,和提供的字段类型,在一个方法中该方法须要什么参数。
*CLR的代码验证程序使用元数据确保你的代码执行时类型安全。
*元数据容许一个对象的字段系列化为内存块,发送给另外一个机器,而后反系列化,在远程机器上重建对象的状态。
*元数据容许垃圾回收器跟踪对象的生命周期。对于任何对象来讲,垃圾回收器能决定对象是何种类型,经过元数据,知道哪个对象所包含的字段被另外一个对象引用。
在第二章,"生成,打包,部署,管理程序和类型",我将讲更多的元数据细节。
微软的C#,Visual Basic,F#,和IL Assembler老是生成包含托管代码(IL)和托管数据(回收数据类型)的模块。为了执行包含托管代码或者托管数据的模块,最终用户必须在他们的机器安装了CLR(目前做为.NET Framework的一部分),一样的,他们也须要安装Microsoft Foundation Class(MFC)库或者Visual Basic DLLs才能运行MFC或者Visual Basic 6.0程序。
默认的,微软的C++编译器生成包含非托管(本地)代码和能够操做非托管数据(本地内存)EXE/DLL的模块在运行时。这些模块不须要CLR执行。不管如何,经过指定CLR命令行转换,C++编译器生成的模块将包含托管代码,这样一来,要执行这些代码就须要安装CLR了。微软全部的编译器都提到,C++是惟一编译器可容许程序员写托管和非托管代码的编程语言而且放到一个模块中。C++也是微软编译器惟一容许开发者在源码中定义托管和非托管数据类型的语言。和其余编译器相比微软的C++编译器的灵活性是无以伦比的,由于它容许开发者使用已存在的本地托管C/C++代码而且开始集成开发者看到适合的托管类型。
Combining Managed Modules into Assemblies(组合托管模块为程序集)
CLR实际上不是依靠模块工做,而是依赖程序集。程序集是一个抽象概念刚开始很难领会。首先,一个程序集是一个逻辑组对应一个或多个模块或源文件集。第二,一个程序集是可重用的、安全的、版本化的最小单元。根据你使用的编译器或工具你能够选择生成一个文件或一个多文件程序集。在CLR的世界里,一个程序集就是咱们叫的组件。
在第二章中,我将十分详细的重温程序集,因此我不打算在这花太多时间在程序集上。如今我所要作的是让你知道一个额外的概念,一种把一组文件看成一个实体的思路。
下图应该能够帮助解释程序集是什么。在这张图中,一些托管模块是被一个工具加工过的源文件(或数据)文件。这个工具每产生一个单独的PE32(+)文件就表明着一个通过逻辑分组的文件集。这个PE32(+)文件包含了一块数据被称做载货单。载货单是元数据表其中一个简单的集合。这些表描述了文件如何组成程序集合,公开导出类型实现的文件在集合中,而且资源或者数据文件与程序集合都有关联。
上图指出了如何把托管模块组装到程序集。
默认的,编译器实际作的工做是把分散的托管模块转换成一个程序集合;C#编译器发出一个托管模块包含一个载货单。载货单现实一个程序集仅仅只由一个文件组成。因此,对于只有一个托管模块而且没有资源(或数据)文件的项目来讲,程序集就是托管模块,当你生成程序的时候不须要任何附加的步骤。若是你要把一个文件集合分组生成程序集,那么你不得不知道更多的工具(好比程序集连接者,AL.exe)和它们的命令行选项。我将在第二章中解释这些工具和选项。
一个程序集容许你以可重用、安全的、无版本冲突的理念在逻辑和物理上解耦。你怎么划分你的代码和资源到不一样的文档彻底取决于你。好比说,你能够把不多用的类型或者资源放到分离的文档中,它是程序集的一部分。分离的文档在运行须要的时候会从网络上下载。假如文档历来没有被用到,该文档永远不会被下载,节省了磁盘空间和减小安装时间。程序集容许你打碎部署文件,可是依然把全部的文件看作一个集合。
一个程序集的模块也包括被引用程序集的信息(包括它们的版本号)。这个信息是一个程序集的自描述。换句话说,CLR能够肯定程序集的直接依赖以便代码在程序集中能正确执行。在注册表或者活动目录域服务(Active Directory Domain Services,AD DS)中是不须要附加信息的。因为不须要附加信息,部署程序集比部署非托管组件容易多了。
Loading the Common Language Runtime(加载公共语言运行库)
每个你生成的程序集都是一个可执行应用程序或者一个DLL(这个DLL包含一个执行应用程序的类型集合)。固然,CLR负责管理包含在这些程序集中的代码。这意味着必须在主机上安装.NET Framework。微软已经建立了一个再分布的包你能够免费下载安装.NET Framework到你的客户的机器上。一些版本的Windows已经自带了.NET Framework。
你能够经过查找%SystemRoot%\System32目录下的MSCorEE.dll文件肯定.NET Framework是否已经正确安装。文件存在则说明.NET Framework已经安装了。不管如何,在一台机器上能够同时安装几个版本的.NET Framework。假如你要肯定哪个版本的.NET Framework确实被安装了,检查如下的子目录。
%SystemRoot%\Microsoft.NET\Framework
%SystemRoot%\Microsoft.NET\Framework64
.NET Framework SDK包含一个叫作CLRVer.exe的命令行工具,这个工具能够显示出全部的安装在机器上的.NET Framework。这个工具还能够显示出哪个原本的CLR正被程序使用,输入-all开关或者输入你感兴趣的进程ID以查看。
在咱们开始考虑CLR怎样加载以前,咱们须要花费一点时间讨论32位和64位版本的Windows。假如你的程序集文件只包含类型安全的托管代码,那么你写的代码应该能够在32位和64位版本的Windows上运行。不须要改变你的源代码以适配两个版本的Windows。事实上,编译器产生的EXE/DLL文件应该能正确运行不管是x86仍是x64版本的Windows。此外,微软商店的应用程序或者类库也能够在Windows RT机器(使用ARM CPU)上运行。换句话说,一个文件能够在任何机器上运行只要在该机器上安装了相应的.NET Framework。
在极限少数场景,开发者要写指定版本的Windows的代码。当使用非安全代码或者当与非托管代码交互操做的时候开发者可能须要写指定CPU架构的代码。为了帮助这些开发者,C#编译器提供了一个平台命令行开关。这个开关容许你指定是否结果集能够运行在x86机器只在32位的Windows版本运行,64位机器只运行64位Windows,或者ARM机器只运行32位Windows RT。假如你不指定平台,默认的是anycpu,这意味着结果程序集能够在任何版本的Windows上运行。VS使用者能够在项目上设置目标平台经过显示项目的属性页,点击生成标签,而后选择目标平台。
在下图中,你会发现首选32位选择框。这个选择框只有在目标平台设置为Any CPU时才会启用,而且项目是一个可执行的类型。若是你勾中首选32位,那么Visual Studio的C#编译器指定平台:anycpu32bitpreferred编译器开关。这个选项指出可执行文件应该在32位的机器上执行,即便正在64的机器上运行。假如你的应用程序不要求额外的内存以运行64位程序,那么勾中首选32位是一个有表明性的好方法由于Visual Studio不支持对x64程序编辑并继续。此外,32位程序能和32位DLL和COM组件交互操做假如你的程序须要。
依赖于平台开关,C#编译器将会发行一个包含PE32或PE32+头文件的程序集。编译器也会在头文件中发行一个须要的CPU架构(或者不可知的架构)。微软搭载了两个SDK命令行工具,DumpBin.exe和CorFlags.exe,经过编译器你能够用来检查发布在托管模块中的头信息。
当运行一个可执行文件时,Windows检查这个EXE文件的头文件肯定应用程序是须要32位的仍是64位的地址空间。一个带有PE32头文件的文件能够在32位或者64位的地址空间运行,可是一个带有PE32+头文件的文件须要64位地址空间。Windows也检查嵌入在头文件中的CPU架构信息以确保电脑上的CPU类型匹配。最后,64位Windows提供了一种技术容许32位Windows应用程序运行。这个技术叫作WoW64(for Windows on Windows 64)。
下图显示了两件事情。第一,它显示了当你给C#编译器指定何种平台命令行开关你将获得何种托管模块。第二,它指出了各类应用程序能在哪些版本的Windows上运行。
当生成模块和在运行时在各个平台的效果
在Windows已经检查了EXE文件的头文件后决定是否建立32位或64位程序,而后Windows加载x86,x64或者ARM版本的MSCorEE.dll到程序的地址空间。在一个x86或ARM版本的Windows上,32位版本的MSCorEE.dll将在%SystemRoot%\System32文件夹下找到。x64版本的Windows上,x86版本的MSCorEE.dll将在%SystemRoot%\SysWow64文件夹下找到。反之64位版本的能够在%SystemRoot%\System32文件夹下找到(因为向后兼容的缘由)。而后程序的主线程调用一个定义在MSCorEE.dll中方法。这个方法会初始化CLR,加载EXE程序集,而后调用入口标点方法(Main)。在这个标点,托管程序启动而且运行。(你的代码能够查询环境的Is64BitOperatingSystem属性肯定程序是不是64位的Windows版本。你的代码也能够查询环境的Is64BitProcess属性肯定是否使用64位地址空间。)
注意 使用1.0或1.1版本的微软C#编译器生成的程序集将包含一个PE32的头文件而且是CPU架构无关的。不管如何,加载时,CLR认为这些程序集都是只生成x86的。对于执行文件,这提升了应用程序确实能够在64位操做系统上工做的可能性,由于可执行文件将会加载WoW64,给进程与32位x86类似的环境。 |
假如一个未托管应用程序调用Win32 LoadLibrary功能加载托管程序集,Windows知道要加载和初始化CLR(假如未加载)去处理包含程序集的代码。固然,在这样的情景下,程序已经启动并运行了,而且可能影响到程序集的可用性。好比,打开解决方案平台x86开关编译的托管程序集就毫不会加载到64位程序,反之,在一个运行64位版本Windows的电脑上打开x86开关编译的可执行文件将会在WoW64中加载。
Executing Your Assembly’s Code(执行你的程序集代码)
就像以前提到过的同样,托管程序集包含元数据和IL。IL是一个不依赖CPU的机器语言,微软在通过几回外部商业和学术言语编译器编写者的讨论后建立了它。IL是一个比较高级的言语相比于大多数CPU机器语言来讲。IL能够访问和操做对象类型并发出指令建立和初始化对象,调用对象中的虚拟方法,直接操做数组元素。它甚至能够发出指令处理异常。你能够认为IL是一个面向对象的机器语言。
一般,开发者会用一个高级语言,好比C#,Visual Basic,或者F#。编译器针对这些高级语言会生成IL。然而,像其余的机器语言同样,IL也可使用汇编语言编写,而且微软提供了一个IL汇编,ILAsm.exe。微软也提供了一个IL反汇编,ILDasm.exe。
记住,任何高级语言最多只会暴露CLR的一个工具子集。可是,IL汇编语言容许开发者访问CLR的全部工具。因此,当你要为你的编程语言选择一个对你有利的CLR隐藏工具时,你能够选择用IL汇编写你的那部分代码或者另外一种包含你须要的特性的编程语言。
惟一了解CLR提供了什么工具的方法是阅读CLR本身提供的文档。在这本书中,我尽可能把重点放在CLR的特征上,经过C#语言怎么暴露或不暴露这些特征。我猜大多数其余的书或者文章将会经过一门语言的视角呈现CLR,而且大多数开发者相信CLR暴露的工具只限于开发者选择的语言。只要你的语言容许你完成你想要完成的,这个模糊的观点就不是坏事。
重点 我认为在能在语言之间经过丰富的集成简单转换开发语言的能力是CLR很是棒的特征。不幸的是,我也相信开发者经常会忽略这个特征。开发语言好比C#和Visual Basic是很好的执行I/O操做的语言。对于执行高级工程学或者财政计算APL是一门很好的语言。经过CLR,你能够在你的应用程序中使用C#写I\O部分,使用APL写工程学计算部分。CLR在这些编程语言之间提供了一层集成,这是空前的,在有的项目中使混合语言编程值得列入考虑。 |
要执行一个方法,它的IL必须先转换成本地CPU指令。这是CLR实时编译器(just-in-time compiler)的工做。
下图示例是方法第一次被调用发生了什么。
仅仅在Main方法调用前,CLR检测全部被Main的代码引用的到的类型。这是由于CLR要分配一个内部数据结构用于管理引用类型的访问。在下图中,Main方法适用于一个单类型,Console,成为单类型的缘由是CLR分配单个内部结构。在Console类型中定义后,针对每一个方法这个内部结构包含一个入口。每一个入口持有一个方法的实现地址。当初始化这个结构时,CLR把每一个入口设置为内部的,CLR本身包含非文档化的功能。我把这个功能叫作JITCompliler(JIT编译器)。
当Main第一次调用WriteLine时,JITCompiler功能被调用了。JITCompiler功能负责编译方法的IL代码生成本地CPU指令。由于IL是被实时编译的,CLR这个组件经常被归为一个JITter或者一个JIT编译器。
注意 假如应用程序在x86版本的Windows上运行或者使用WoW64技术,JIT编译器生成x86指令。假如你的应用程序是64位版本而且在x64版本的Windows上运行,JIT编译器生成x64指令。假如应用程序在ARM版本的Windows上运行,那么JIT编译器生成ARM指令。 |
第一次调用一个方法
当被调用时,JITCompiler功能知道什么方法被调用了和什么类型定义这个方法。JITCompiler功能而后搜索定义程序集的元数据以调用方法的IL。JITCompiler接下来验证和编译IL代码生成本地CPU指令。本地CPU指令被保存在动态分配内存块中。而后,JITCompiler返回到CLR建立的类型的内部数据结构并调用方法入口,而后使用刚编译好的包含本地CPU指令的内存块地址替换第一次调用它的引用地址。最后,JITCompiler功能跳到内存块代码。这份代码是WriteLine方法的实现(一个包含String参数的版本)。当这份代码返回,它返回到在Main中的代码,代码继续正常执行。
Main如今第二次调用WriteLine。此次,WriteLine代码已经被验证和编译过了。因此调用直接去到内存块,彻底跳过JITCompiler功能。在WriteLine方法执行后,它返回到Main。下图显示了当WriteLine第二次被调用时程序看上去是什么样。
只有第一次调用方法时才会有性能损失。随后全部的方法调用执行本地代码都会全速执行,由于对于本地代码不会再执行验证。
JIT编译器在动态内存中存储本地CPU指令。这意味着当应用程序结束时编译过的代码会被丢弃。假如你要再运行一次应用程序或者你同时运行两个应用程序实例(在两个不一样的操做系统进程),JIT编译器将再把IL编译成本地代码一次。依赖于应用程序,这将明显的增长内存消耗相比较于一个本地应用程序,这个本地应用程序的只读代码能被全部运行实例共享。
对于大多数应用程序来讲,JIT编译影响的性能并不明显。大多数应用程序倾向于一遍又一遍的调用相同的几个方法。这些方法只会在应用程序执行时影响一次性能。它也可能花费更多时间在方法内部相比于只调用方法。
你应该知道CLR的JIT编译器优化本地代码就像后端的一个非托管C++编译器同样。然而,它可能会花费更多的时间以优化代码,可是与未优化的代码相比优化过的代码会有更佳的性能。
有两个C#编译器开关会影响代码优化:/optimize和/debug。下面的图表显示了这些开关对代码质量的影响,经过C#编译器生成的IL代码和经过JIT编译器生成的本地代码。
打开/optimize-,C#编译器生成的非优化代码包含不少非操做(NOP)指令和跳到下一行代码的分支。这些指令发布是为了在调试时启用Visual Studio的edit-and-continue特征,额外的指令容许在流程控制指令好比for,while,do,if,else,try,catch,和finally声明块设置断点以便代码更容易调试。当生成优化的IL代码时,C#编译器将会移除无关的NOP和分支指令,使代码难以单步调试由于流程控制被优化了。一样的,一些功能评价在内部调试时可能也不工做了。不管如何,IL代码更少了,结果是EXE/DLL文件也更小了,对于喜欢查看编译器生成的IL的人来讲,IL更好阅读了。
此外,在你指定/debug(+/full/pdbonly)开关后编译器会生成一个Program Database(PDB)文件。PDB文件帮助调试器找到本地变量并把IL指令映射到源代码。/debug:full开关告诉JIT编译器你要调试程序集,JIT编译器将会跟踪来自每一个IL指令的本地代码。这容许你使用Visual Studio的实时调试器(just-in-time debugger)特性,链接到一个调试器上调试已经在运行的进程。没有/debug:打开full开关,JIT编译器的默认设置不会跟踪IL到本地代码信息,这使得JIT编译器运行的稍微快一点和使用稍少的内存。假如你用Visual Studio调试器开始一个进程,它会强制JIT编译器跟踪IL到本地代码信息(无论/debug开关)除非你关掉Visual Studio中在模块加载时取消JIT优化(仅限托管)选项。
当你在Visual Studio中建立一个新的C#项目,Debug配置项有/optimize-和/debug:full开关,在Release配置项中指定了/optimize+和/debug:pdbonly开关。
对于那些来自带有非托管C或者C++背景的开发者来讲,你可能考虑的是这一些以外的性能分支。毕竟,非托管代码编译针对的是一个指定的CPU平台,而且,当被调用后,代码能够简单的执行。在托管的环境中,须要两个步骤完成代码编译。首先,编译器忽略源码,作尽量多的工做生成IL。可是要执行代码,在运行时IL本身必须编译成本地CPU指令,须要分配更多的不可共享内存和须要额外的CPU时间工做。
相信我,由于我本身是以C/C++背景接触CLR,我是十足的怀疑论者并关注这些额外的开销。事实是在运行时发生的第二次编译阶段并不损伤性能,它也不分配动态内存。不管如何,微软为了保持额外的开销最小已经在性能上作了不少工做。
假如你也是怀疑论者,你固然应该建几个应用程序测试性能。此外,你应该运行几个微软或者其它生成的不凡的托管应用程序,并测试它们的性能。你想你应该会惊讶的发现实际性能怎么会那么好。
你可能以为这难以置信,可是不少人(包括我)认为托管应用 程序赛过非托管程序。有不少缘由证实这个。好比,运行时当JIT编译器编译IL代码成本地代码,编译器比非托管代码编译器知道更多执行环境。这里是一些托管代码能够赛过非托管代码的方法:
*JIT编译器能够判断应用程序是否能够在Intel奔腾4CPU上运行,利用奔腾4提供的特殊指令生成本地代码。一般,非托管应用程序是为最小公分母CPU编译的并避免使用特殊的指令那将给应用程序一个性能提高。
*JIT编译器能够运行时判断某个测试老是false。列如,考虑一个方法包含如下代码。
if(numberOfCPUs>1) { ... }这些代码能够引发JIT编译器不生成任何CPU指令假如主机上只有一个CPU。假如这样,对于主机,本地代码将会微调;结果代码将会更小而后执行的更快。
*CLR能够给出代码的执行轮廓和重编译IL成本地代码当应用程序运行时。依赖于观察执行模型预报,重编译代码能够被重组减小错误分支。当前版本的CLR不作此工做,可是将来的版本可能会。
这些是几个缘由使你期待将来的托管代码比今天的非托管代码执行的更好。正如我所说,对于大多数应用程序如今已经有足够好的性能,而且随着时间的推动会有所改善。
假如你的实验显示出CLR的JIT编译器没有提供给你的应用程序须要的性能,你可能要利用装载在.NET Framework SDK中的NGen.exe工具。这个工具编译程序集中全部的IL代码成本地并把结果保存在一个文件中存在磁盘上。在运行时,当程序集被加载完成,CLR自动检查是否已经有一个程序集的预编译版本存在,假若有,CLR加载预编译代码而不须要在运行时再编译。注意NGen.exe必须保存它关于实际执行环境的假设,为了这个缘由,NGen.exe产生的代码将不会像JIT编译器生成的代码同样高度优化。在这一章稍后我将讨论更多关于NGen.exe的细节。
此外,你能够须要考虑使用System.Runtime.ProfileOptimization类。当你的应用程序运行时,这个类使CLR记录(到一个文件)JIT编译了什么方法。而后,将来你的应用程序启动时,JIT编译器将会同时用其余的线程编译器这些方法假如你的应用程序是在一个多CPU的机器上运行。最后的结果是你的应用程序运行的更快,由于多个方法被同时编译了,而且在应用程序初始化期间,代替原来用户与你的应用程序交互时的实时编译。
IL and Verification(中间语言和验证)
IL是以堆栈为基础的(stack-based),这意味着它的全部指令,都是把操做数都压入一个执行堆栈,弹出结果出栈。由于IL没有提供指令操做寄存器,人们很容易就能够建立一门新的语言并把它编译产生的代码以CLR为目标。
IL指令也是无类型的。列如,IL提供一个加(add)指令把压入堆栈中的最后两个操做数加起来。32位和64位版本的加指令没有分别。当加指令执行时,它决定堆栈中的操做数类型并执行适合的操做。
在我看来,IL最大的好处不是抽离于CPU底层。IL的最大好处是提供给应用程序健壮性和安全性。当编译IL成本地CPU指令时,CLR执行一个叫作verification的进程。Verification检查高级IL代码并确保代码要作的事情是安全的。列如,verification检查每个被调用的方法都有正确的参数数目,每一个参数传到每一个方法都有正确的类型,每一个方法的返回值使用得当,每一个方法有一个返回声明,等等。托管模块的元数据包括全部用于verification进程的方法和类型信息。
在Windows中,每一个进程都有本身的虚拟地址空间。分离的地址空间十分必要,由于你不能信任一个应用程序的代码。一个应用程序彻底可能(不幸的是,太常见了)读或写一个无效的内存地址。经过把各个Windows进程放到分离的地址空间,你能够增长健壮性和稳定性;一个进程不会严重影响另外一个进程。
经过验证托管代码,不管如何,你知道代码不适合访问内存和不会影响另外一个应用程序的代码。这意味着在一个Windows的虚拟地址空间你能够运行多个托管应用程序。
由于Windows多个进程须要不少操做系统资源,持有多个进程会损耗性能和限制资源可用性。经过在一个操做系统进程中运行多个应用程序减小进程数量能够改善性能,须要更少的资源,并像每一个应用程序持有进程同样稳健。这是托管代码的另外一个好处和非托管代码相比。
实际上CLR作的,是提供使多个托管应用程序在一个操做系统进程执行的能力。每一个托管应用程序在一个AppDomain里执行。默认的,每一个托管EXE文件将会在它本身的分离地址空间运行,并只有一个AppDomain。然而,一个持有CLR的进程(好比IIS【Internet Information Service】或者Microsoft SQL Server)能够决定在一个操做系统进程中运行多个AppDomain。
Unsafe Code(不安全代码)
默认的,Microsoft的C#编译器生成安全代码。安全代码是指验证安全的代码。然而,Microsoft的C#编译器运行开发者写不安全的代码。非安全代码运行直接在内存地址运行并能够在这些地址中操做字节。这是一个很是强大的特色而且是经常使用的,当你和非托管代码互操做或者当你想要改变时序要求严格的算法性能。
不管如何,使用非安全代码介绍了一个重大风险:非安全代码会破坏数据结构并利用或甚至打开安全弱点。因为这个缘由,C#编译器要求全部包含非安全代码的方法标记unsafe关键字。此外,C#编译器须要你打开/unsafe编译器开关编译源码。
当JIT编译器尝试编译一个非安全代码,它要检查程序集的方法是否已经被System.Security.Permissions.Security Premission经过System.Security.Permissions.SecurityPermissionFlag’s SkipVerfication设置标记。假如这个标记已经设置了,JIT编译器将会编译非安全代码并容许执行。CLR信任这些代码并但愿直接地址和字节操做不会引发任何伤害。假如这个标记没有设置,JIT编译器抛出一个System.InvalidProgramException或一个System.Security.VerificationException错误,阻止方法执行。实际上,整个应用程序将会在这个点中止,可是至少不会形成任何伤害。
注意 默认的,从本地机器或者经过network(多台计算机的链接的网络)分享的程序集是被信任的,意味着它们能够作任何事,包括执行非安全代码。然而,默认的,经过Internet网络下载的程序集是不会被受权执行非安全代码的。假如它们包含非安全代码,上述提到的异常就会被抛出。一个管理员/最终用户能够改变这些默认设置;不管如何,管理对代码的行为负有全责。 |
微软支持一个叫作PEVerify.exe的工具,它检查一个程序集内的全部方法并通知你任何包含有非安全代码的方法。你可能须要考虑运行PEVerify.exe检查你引用的程序集;这将让你知道你从局域网或者互联网下载的应用程序在运行时是否有问题。
你应该意识到验证须要访问任何包含在依赖程序集中的元数据。因此当你使用PEVerify检查一个应用程序,它必须能够定位和加载全部的引用程序集。由于PEVerify使用CLR定位依赖程序集,经过使用相同的绑定和探索规则定位程序集 ,当运行时应该能够正常执行程序集。我将会在第二和第三章讨论这些绑定和探索规则,“Shared Assemblier and Strongly Named Assemblies.(分享程序集和强命名程序集)”
IL and Protecting Your Intellectual Property(IL和保护你的知识产权) 有的人关心IL没有提供足够的知识产权属性去保护他们的算法。换句话说,他们认为你生成一个托管模块,某我的也可使用一个工具,好比IL Disassembler(IL 反汇编程序),能够简单的反向工程肯定你的代码作了什么。 是的,这是事实,IL代码比起其它的程序集语言是更高级,而且,通常而言,反向工程IL代码相对简单。然而,当实现服务端代码(好比网页服务,网页表格,或者存储过程),你的程序集存在你的服务器上。由于除了你公司人的人之外没人能够访问程序集,除了你公司的人没人可使用任何工具查看IL——你的知识产权彻底安全。 假如你关注任何你发布的程序集,你能够获取一个混淆工具从第三方厂商。这些工具把全部的私有标记的名字混淆在你的程序集元数据中。它将变的十分困难假如某我的要缕清名字和弄懂每一个方法的目的。注意这些混淆器只能提供一点点保护由于IL必须在一些点上对CLR有用,JIT才能编译IL。 假如你不认为混淆器没有提供你想要的知识产权保护,你能够考虑使用一些非托管模块实现更多的敏感算法,非托管模块将会包含本地CPU指令替代IL和元数据。而后你可使用CLR的相互操做特征(假设你又足够的权限),应用程序的托管部分和非托管部分就会通讯。固然,这是假设你不担忧在你的非托管代码中人们会反向工程本地CPU指令。 |
The Native Code Generator Tool:NGen.exe(本地代码生成工具:NGen.exe)
NGen.exe工具装载在.NET Framework上,用户在安装应用程序时,NGen.exe会把IL代码编译成本地代码。由于代码是在安装时编译,CLR的JIT编译不用再运行时编译IL代码,所以改善了应用程序的性能。NGen.exe工具在如下两个场景会显得有趣:
*改善一个应用程序的启动时间 运行NGen.exe能够改善启动时间是由于代码已经被编译成本地代码因此编译不会在运行时发生。
*减小一个应用程序的工做集 假如你相信一个程序集将会同时加载到多个进程,在程序集上运行NGen.exe会减小应用程序的工做集。缘由是NGen.exe工具把IL代码编译成本地代码并把输出保存到一个分离的文件中。这个文件能够同时内存映射到多个进程的地址空间 ,运行代码分析;而不是须要每一个须要的进程拷贝一份代码。
在一个应用程序或一个单程序集上,当一个设置程序调用NGen.exe,应用程序的全部程序集或某个指定的程序集的IL代码会被编译成本地代码。NGen.exe建立一个新的只包含本地代码的程序集文件替代IL代码。这个新的文件放在一个文件夹下,目录名字像%SystemRoot%\Assembly\NativeImages_v4.0.####_64。这个目录名字包括CLR版本和是否本地代码编译成32位或者64位版本Windows的指示信息。
如今,不管什么时候CLR加载一个程序集文件,CLR查找是否有符合NGen的本地文件已经存在。若是找不到本地文件,CLR JIT编译器像往常同样编译IL代码。然而,若是存在符合的本地文件,CLR将会使用包含在本地文件中的编译过的代码,文件中的方法也不会在运行时编译。
表面上,这听起来至关不错!它听起来好像你得到了托管代码的全部好处(垃圾回收,验证,类型安全,等等)而且托管代码彻底没有性能问题(JIT编译)。然而,真实的情况是,它不像第一眼看上去那样美好。关于NGen的文件有几个潜在的问题:
*没有知识产权保护 不少人相信它有可能装运不包含IL代码的NGen文件,从而保护他们的知识产权。不幸的是,这不可能。在运行时,CLR要求访问程序集的元数据(好比实现反射或系列化的功能);这要求程序集包含IL和装运了元数据。此外,假如CLR由于某些缘由(接下来有所描述)不能使用,CLR将优雅的返回JIT编译程序集IL代码,这样必定可行。
*NGen文件能够摆脱同步 当CLR加载NGen文件,关于以前编译好的代码和当前环境,CLR将会比较大量的特征。假若有任何一点特征不匹配,NGen文件就不能使用,这时进入正常的JIT编译器进程。这里是部分必须匹配的特征清单:
-CLR 版本:补丁或服务包改变时CLR版本也将改变。
-CPU 类型:假如你升级你的处理器硬件CPU类型将会改变。
-Windows操做系统版本:一个新的服务包更新Windows系统版本也会更新。
-程序集的身份模块版本ID( module version ID,MVID):当从新编译后模块版本ID将改变。
-引用程序集的版本ID:当你从新编译一个引用的程序集时改变。
-安全:当你取消那些以前受权过的许可(好比声明继承,声明link-time,SkipVerification,或者UnmanagedCode 许可)会引发安全改变。
注意 有可能在更新模型中运行NGen.exe。这告诉工具在之前已是NGen文件的全部程序集中运行NGen.exe。不管什么时候一个最终用户安装一个新的.NET Framework服务包,服务包的安装程序都会在更新模型中自动运行NGen.exe以便NGen文件能够和安装的CLR版本保持同步。 |
*低下的执行时间性能 当编译代码时,NGen不能像JIT编译器同样作多个关于执行环境的假定。这是由于NGen.exe产生低质的代码。列如,NGen不会优化特定CPU指令的使用;它为访问静态字段增长了迂回由于静态字段的实际地址只有在运行时才知道。NGen处处插入代码以调用类构造器由于它不知道代码将会以什么顺序执行而且不知道类构造器是否已经被调用过。(见第8章,“Methods”,更多关于类构造器的内容。)一些 NGen的应用程序执行比它们的JIT编译副本实际慢大概5%。因此,假如你考虑使用NGen.exe提高你的应用程序性能,你应该比较NGen和非NGen版本确保NGen版本实际上运行并不慢!对于一些应用程序来讲,减少工做集提升性能,那么使用NGen是极大的优点。
归功于刚才列出的全部问题,当你考虑NGen.exe你应该很谨慎。对于服务端应用程序,NGen.exe做用很小或者没有用做由于仅在客户端第一次请求时会体验到性能损失;之后的客户端请求都将高速运行。此外,对于大多数服务应用程序,代码只会实例化一次,因此没有工做集的好处。
对于客户端应用程序,NGen.exe可能有意义对于改善启动时间或减小工做集假如一个程序集被同时用于多个应用程序。甚至在程序集不用于多个应用程序的实例中,在程序集中执行NGen能够改善工做集。此外,假如NGen.exe用于全部的客户端应用程序的程序集,CLR彻底不须要加载JIT编译器,进一步减小了工做集。固然,假如一个程序集不是NGen的或者假如一个程序集的NGen文件不可用,那么将加载JIT编译器,应用程序的工做集也就变大了。
对于大型客户端应用程序会体验一个很长的启动时间,Microsoft提供一个管理文件导向优化工具(Managed Profile Guided Optimization,MPGO.exe)。这个工具分析执行你的应用程序时须要启动什么。为了优化生成本机映像这个信息将会反馈给NGen.exe工具。这容许你的应用程序启动更快并减小工做集。当你准备装运你的应用程序,凭借MPGO工具启动它而后操练你的应用程序的公共任务。你的执行部分的代码信息被写入一个文件,它嵌入在你的程序集文件中。NGen.exe工具使用这个文件数据更好的优化NGen.exe生成的本机映像。
The Framework Class Library(Framework类库)
.NET Framework包含框架类库(Framework Class Library,FCL)。FCL是一个DLL程序集集合包含几千个类型,在每一个类型中暴露了几个功能函数。Microsoft也生成附加的库好比Windows Azure SDK和DirectX SDK。这些附加的库给你的使用提供了更多的类型,暴露了更多的功能函数。实际上,Microsoft生成类库的速度惊人,使类库史无前例的容易当开发者使用各类各样Microsoft技术时。
这里是一些种类的应用程序,开发者建立能够经过这些程序集:
*网页服务(Web services) 方法能够加工信息很容易的发送到因特网经过使用微软的ASP.NET XML Web Service技术或微软的Windows Communication Foundation(WCF)技术。
*网页表单/MVC 以HTML为基础的应用程序(网站) 典型的,ASP.NET应用程序将会作数据库查询和网页服务调用,合并和过滤返回的信息,而后经过丰富的以HTML为基础的用户界面在一个浏览器上呈现信息。
*Rich Windows GUI 应用程序 代替使用网页建立你的应用程序用户界面,你可使用Windows商店提供的更强大的,更高性能的功能,Windows Presentation Foundation(WPF),或者Windows Forms技术。GUI应用程序能够利用控件、菜单、和触摸、鼠标、触控笔和键盘事件的好处,GUI应用程序能够直接和操做系统底层交换信息。Rich Windows应用程序也能够作数据库查询和使用网页服务。
*Windows console应用程序 对于简单UI要求的应用程序,一个console应用程序提供了快捷简单的方法生成一个应用程序。编译器,工具都是典型的做为console应用程序实现的。
*Windows services 是的,经过使用.NET Framework的Windows Service Control Manager(SCM)有可能生成一个可控的服务应用程序。
*数据库存储过程(Database stored procedures) 微软的SQL Server,IBM的DB2,和Oracle的数据库服务运行开发者经过使用.NET Framework写他们本身的存储过程。
*组件库(Component library) .NET Framework容许你生成独立的程序集(组件),程序集(组件)包含的类型能够容易的合并到任何以前提到的应用程序类型中。
重要 Visual Studio容许你建立一个Portable Class Library(可移植类库)项目。这个项目类型让你建立一个单独的类库程序集能够在各类应用程序类型工做,包括.NET Framework自己,Silverlight,Windows Phone,Window Store应用和Xbox360。 |
由于FCL不夸张的包含成千上万的类型,在一个命名空间里有一个相关类型的集合呈现给开发者。列如,System命名空间(你应该变的很熟悉的一个命名空间)包含基础类型Object,其余的全部类型都继承了Object。此外,System命名空间包含的类型还有integers,characters,strings,exception handling,和console I/O一串数据类型之间的安全转换工具类型,转换数据类型,生成随机数,和执行各类数学功能。全部的应用程序都将使用System命名空间下的类型。
要访问框架的任何特性,你须要知道哪一个命名空间包含类型暴露的功能函数以后。不少类型容许你自定义他们的行为;那你能够经过简单的从你须要的FCL类型继承到你本身的类型。平台的面向对象本质就是.NET Framework怎么样呈现一致的编程范式给开发者。同时,开发者能够容易的建立包含他们本身类型的命名空间。这些命名空间和类型无缝的合并到编程范式里。和Win32编程范式相比,这种新的方式极大的了简化了软件开发。
大多数在FCL呈现的类型的命名空间能够在任何种类的应用程序中使用。下表列出了一些更加通常的命名空间和简单描述,在那个命名空间什么类型被使用了。这是可用的命名空间很小的样本。随着微软日益增加的命名空间集合,请看文件伴随着各类微软SDK以增长熟悉度。
这本书是关于CLR并和CLR紧密相互做用的通常类型。因此这本书的内容适用于全部写应用程序的开发者或者以CLR为目标的组件。存在其它不少好书,涉及指定应用程序类型好比Web Services,Web Forms/MVC,Windows Presentation Foundation,等等。这些书在帮助你生成你的应用程序方面会给你一个很好的开始。我倾向于认为这些指定应用程序的书帮助你自上而下的学习由于它们专一于应用程序类型而不是开发平台。在这本书中,我将拿出信息帮助你自下而上的学习。在你读了本书和一本指定应用程序的书后,你应该能够简单熟练的创建任何种类你想要的应用程序。
The Common Type System(公共类型系统)
目前为止,很明显CLR都是关于类型的。类型暴露功能函数给你应用程序和其它类型。类型是经过一种编程语言写的代码能够和不一样的编程语言谈话的机制。由于是CLR的根本,微软建立了一个形式规范——公共类型系统(Common Type System,CTS)——描述了类型怎样定义和它们怎样运行。
注意 实际上,微软已经提交了CTS作为.NET Framework的其它部分,包括文件转换,元数据,IL和访问平台低层(P/Invoke)达到ECMA(欧洲计算机制造联合会)为了实现标准化目标。标准叫作公共语言基础设施(Common Language Infrastructure,CLI)而且是ECMA-335规格。此外,微软还提交了FCL部分,C#编程语言(ECMA-334),和C++/CLI 编程语言。关于这些工业标准的信息,去ECMA网站查看,属于技术委员会39(Technical Committee 39,TC39):http://www.ecma-international.org。你也能够参考微软本身的网站:http://msdn.microsoft.com/en-us/netframework/aa569283.aspx 。此外,微软已经对ECMA-334和ECMA-335规格应用了他们的社区承诺。 |
CTS规格声明了一个类型能够包含0个或多个成员。在第二部分,“设计类型(Designing Types)”,我会很详细的讲述全部成员。如今,我只是给你简单的介绍一下它们:
*字段(Field) 一个数据变量,是对象声明的一部分。字段经过它们的名字和类型被识别为字段。
*方法(Method) 一个功能,在对象中执行一个操做,常常改变对象的声明。方法有一个名字,一个签名,和修饰词。签名指定参数的数量(和它们的顺序),参数的类型,方法是否有返回值,若是这样,方法返回值的类型。
*属性(Property) 对于调用者,这个成员看上去像是一个字段。可是对于类型实现者,它像一个方法(或者两个方法)。必要时,属性容许一个实现者验证输入参数和对象声明在访问数值和/或计算一个数值以前。它们也容许类型使用者有简单的语法。最后,属性容许你建立只读或只写的“字段”。
*事件(Event) 一个事件容许一个通知机制在一个对象和其它感兴趣的对象之间。例如,一个按钮能够拿出一个事件通知其它的对象当按钮被点击的时候。
CTS也制定类型可见度和类型成员访问规则。例如,把一个类型定义为公共的(叫作public)输出类型,使它可见和可访问对于任何程序集。在另外一方面,把一个类型看成程序集(在C#中调用internal),代码可见和可访问都只能在同一程序集中。所以,CTS经过程序集为类型造成的可见边界创建规则,CLR实施可见规则。
一个调用者可见的类型能够进一步限制调用者访问类型成员的能力。下面的清单显示了控制访问一个成员的有效选项:
*Private 只有在同一个类类型中的其它成员才能够访问该成员。
*Family 派生类型能够访问该成员,无论它们是否是在同一个程序集中。注意,不少语言(好比C++和C#)把family称做protected。
*Family and assembly 派生类型能够访问该成员,可是只有派生类型定义在同一个程序集中。不少语言(好比C#和Visual Basic)不提供这个访问控制。固然,IL汇编语言提供这个访问控制。
*Assembly 同一个程序集中的任何代码均可以访问该成员 。不少语言把assembly称做internal。
*Family or assembly 在任何程序集中派生的类型均可以访问该成员。在同一个程序集中的任何类型均可以访问该成员。C#把family or assembly称做protected internal。
*Public 任何程序集中的任何代码均可以访问该成员。
此外,CTS定义规则控制类型继承、虚拟方法、对象生命周期等等。这些规则已经被设计出来适应当代编程语言的语义表达。实际上,你不须要学习CTS规则自己由于你选择的语言将会使用相同的方式暴露它本身的语法和类型规则。当编译区间它发布程序集时,它会把语言特定的语法映射成IL,CLR的“语言”。
当我第一次用CLR工做时,我立刻发觉它是考虑的最好的语言,把代码的行为做为两个单独和独特的东西。使用C++/CLI,你能够定义你本身的类型和类型的成员。固然,你也可使用C#或者Visual Basic定义相同的类型和类型成员。的确,你定义类型的语法依赖于你选择的语言而不一样,可是类型的行为将会彻底同样忽略使用的语言由于CLR的CTS定义类型的行为。
为了帮助理清这个概念,让我给你一个示例。CTS容许一个类型只能从一个基类继承。因此,即便C++语言支持类型从多个基类继承,CTS不接受和操做任何这样的类型。为了帮助开发者,微软的C++/CLI编译器会报告一个错误假如它发现你试图建立包含一个类型继承多个基类的托管代码。
这里是其余的CTS规则。全部的类型必须(最终)必须继承自一个预先定义好的类型:System.Object。正如你所看到的,Object是一个类型的名字,这个类型定义在System命名空间中。这个Object是其它全部类型的根类型所以确保了每一个实例化类型都有一个最小的行为集合。明确的,System.Object类型容许你作如下的事情:
*比较两个实例是否相等。
*为实例获取哈希码。
*获取一个实例的类型。
*为当前实例建立一个浅副本。
*获取一个表示当前实例的字符串。
The Common Language Specification(公共语言规格)
COM容许用不一样的语言建立能够互相通讯的对象。另外一方面,CLR如今集成了全部语言并容许一种语言建立的对象和另外一种彻底不同的语言代码写的做为平等公民对待。这种集成成为多是由于CLR的类型、元数据(类型自描述信息)和公共执行环境的标准集合。
即使这个语言集成是一个漂亮的进球,事情的真相是各个编程语言之间有很大的区别。例如,一些语言大小写不敏感,一些不提供无符号整数,操做符重载,或者方法支持可变数量的参数。
假如你倾向于建立一个其它编程语言也能容易的访问的类型,你须要使用你的编程语言仅有的特征确保在其它语言都有效。为了帮助你实现这个,微软定义了一个公共语言规格(Common Language Specification,CLS)细节给编译器供应商,他们的编译器必须支持最小特征集合假如它们的编译器要生成的类型兼容其它组件,这些组件是经过在CLR上符合公共语言规范(CLS-compliant)的语言写的。
CLR/CTS支持的特征比在CLS子集中定义的特征多,因此假如你不关心不一样语言间的可操做性,你能够开发不少类型且只限于语言的特征集合。确切的,外部可见的类型和方法必须追随CLS定义的规则假如它们能够被任何符合公共语言规范(CLS-compliant)的开发语言访问。注意CLS规则不适用于只在程序集内的可访问性。下图总结了这一点表达的观点。
以下图所示,CLR/CTS提供了一个特征集合。一些语言暴露CLR/CTS一个大的子集。例如,一个开发者将使用IL汇编语言写代码,他可使用CLR/CTS提供的全部特征。其余的大多数语言,好比C#,Visual Basic,和Fortran,暴露了CLR/CTS的一个子集给开发者。CLS定义的最小特征集合全部语言都必须支持。
假如你只用一种语言设计类型,并但愿类型能够被其它语言使用,你不能在类型的public和protected成员中利用CLS以外的任何特征。这么作意味着你的类型成员不能被开发者用另外一种语言写的代码访问。
接下来的代码中,一个符合公共语言规范(CLS-compliant)的类型将定义在C#中。可是,类型包含几个不符合公共语言规范(non-CLS-compliant)的构造引发C#编译器抱怨代码。
using System; [assembly:CLSCompliant(true)] namespace SomeLibrary { public sealed class SomeLibraryType { // warning CS3002: “SomeLibrary.SomeLibraryType.Abc()”的返回类型不符合 CLS public UInt32 Abc() { return 0; } //warning CS3005: 仅大小写不一样的标识符“SomeLibrary.SomeLibraryType.abc()”不符合 CLS public void abc() { } //no warning:这个方法是私有的 private UInt32 ABC() { return 0; } } }
在这份代码中,[assembly:CLSCompliant(true)]属性被应用于程序集。这个属性告诉编译器以确保任何公开暴露没有任何构造的类型将会阻止类型被任何其它编程语言访问。当代码被编译后,C#编译器发布两个警告。第一个警告报告了由于方法Abc返回一个无符号整数。一些其它的编程语言不能操做无符号整数值。第二个警告是由于这个类型暴露两个公共方法只有大小写和返回类型不一样。Visual Basic和其余的一些语言不能同时调用这些方法。
有趣的是,假如你删除了sealed class SomeLibraryType前的public并从新编译,两个警告都会消失。缘由是SomeLibraryType将会默认为internal从而程序集再也不暴露在外面。完整的CLS规则列表,参考在.NET Framework SDK文档中的“跨语言互操做性(Cross-Language Interoperability)”章节(https://msdn.microsoft.com/zh-cn/library/730f1wy3.aspx)。
让我把CLS规则很简单的提取出来。在CLR中,类型的每一个成员要么是个字段(数据)要么是个方法(行为)。这意味着每一个编程语言都能访问字段和调用方法。通常的字段和通常的方法用于特殊或经常使用的途径。为了减小编程,语言一般提供额外的抽象使编写这些经常使用编程模式更简单。例如,语言暴露的思想好比枚举、数组、属性、索引器、委托、事件、构造器、终结器、运算符重载、转换操做符等等。当一个编译器在你的源码中遇到这些中的任何一个时,编译器必须把这些构造转换成字段和方法,这样CLR和其它的编程语言才能访问构造。
考虑下面的类型定义,它包含一个构造器,一个终结器,一些运算符重载,一个属性,一个索引器和一个事件。注意在在这的代码只是为了可以编译;而不是正确的实现类型的方式。
using System; namespace SomeLibrary { internal sealed class Test { //构造器 public Test() { } //终止器 ~Test() { } //操做符重载 public static Boolean operator ==(Test t1, Test t2) { return true; } public static Boolean operator !=(Test t1, Test t2) { return false; } //一个操做符重载 public static Test operator +(Test t1, Test t2) { return null; } //一个属性 public String AProperty { get { return null; } set { } } //一个索引器 public String this[Int32 x] { get { return null; } set { } } //一个事件 public event EventHandler AnEvent; } }
当编译器编译代码时,结果是包含有几个字段和方法的类型。你可使用.NET Framework SDK提供的IL反汇编工具(ILDasm.exe)检查生成的托管模块,以下所示。
下表显示了编程语言构造怎么等价映射到CLR字段和方法。
Test类型的字段和方法(从元数据中获取)
类型成员 | 成员类型 | 等价编程语言构造 |
AnEvent | 字段 | 事件;字段的名字是AnEvent,它的类型是System.EventHandler。 |
.ctor | 方法 | 构造器。 |
Finalize | 方法 | 终止器。 |
add_AnEvent | 方法 | Event添加访问器方法。 |
get_AProperty | 方法 | Property获取访问器方法。 |
get_Item | 方法 | 索引器获取访问器方法。 |
op_Addition | 方法 | +操做符。 |
op_Equality | 方法 | ==操做符。 |
op_Inequality | 方法 | !=操做符。 |
remove_AnEvent | 方法 | Event移除访问器方法。 |
set_AProperty | 方法 | Property设置访问器方法。 |
set_Item | 方法 | 索引器设置访问器方法。 |
Test类型下额外的节点在上表中没有说起——.class、.custom、AnEvent、AProperty和Item——识别出类型的附加元数据。这些节点不映射到字段和方法;它们只是提供了一些关于类型的额外信息给CLR、编程语言或者工具访问。例如,一个工具能够看出Test类型提供了一个事件,叫作AnEvent,经过两个方法(add_AnEvent和remove_AnEvent)暴露给外面。
Interoperability with Unmanaged Code(和非托管代码的互操做性)
比起其余的开发平台.NET Framework提供了大量的优点。然而,不多有公司能够负担得起重启设计和从新实现他们如今已有的代码。微软察觉了这一点并构造了CLR,它提供了一个容许应用程序由托管和非托管两部分组成的机制。特定的,CLR支持三个互操做性场景:
*托管代码能够调用一个DLL中包含的非托管功能函数 托管代码经过使用一个叫作P/Invoke的机制调用包含在DLL集合中的功能函数。毕竟,FCL内部定义了不少类型,从Kernel32.ll、User32.dll等等调用暴露的功能函数。不少编程语言暴露一个机制使托管代码调用包含在DLL中的功能函数很容易实现。例如,一个C#应用程序能够调用Kernel32.dll暴露的CreateSemaphore功能函数。
*托管代码可使用一个存在的COM组件(服务) 不少公司已经实现了大量的非托管COM组件。使用在这些组件中的类型库,描述COM组件的一个托管程序集将会建立。托管代码能够访问托管程序集中的类型就像访问其它的托管类型同样。查询装载在.NET Framework SDK中的Tlblmp.exe了解更多信息。有时,你可能没有一个类型库或者对于Tlblmp.exe产生的内容你可能想要更多的控制权。若是这样,你能够手动在源码中创建一个类型,CLR能够用于获取合适的互操做性。例如,你能够在一个C#应用程序中使用DirectX COM组件。
*托管代码可使用一个托管类型(服务) 不少已经存在的非托管代码要求你供给一个COM组件使代码能正确工做。使用托管代码实现这些组件容易得多,你能够避免全部代码都要引用计数和接口。例如,你可使用C#建立一个ActiveX控件或外壳扩展程序。查看装载.NET Framework SDK中的TlbExp.exe和RegAsm.exe工具了解更多信息。
注意 微软如今为类型库导入器(Type Library Importer)工具提供源码和P/Invoke互操做助手(Interop Assitant)帮助开发者和本地代码交互。这些工具和源码能够从http://CLRInterop.CodePlex.com/ 下载。 |