微软.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。
下图指出了程序编译源码文件。如图所示,你可以使用任何支持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。列如,考虑一个方法包含以下代码。
这些代码可以引起JIT编译器不生成任何CPU指令假如主机上只有一个CPU。假如这样,对于主机,本地代码将会微调;结果代码将会更小然后执行的更快。if(numberOfCPUs>1) { ... }
*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/ 下载。 |