本章介绍一些基本概念,譬如程序集、元数据和公共语言运行时(CLR)。你将会了解到如何编写客户端和类库程序集,以及某一程序集的客户端如何使用另一程序集中的二进制组件。本章随后讨论了.NET如何实现二进制兼容,同时展示了.NET如何支持先前章节介绍过的这一面向组件的核心原则。
语言独立性:公共语言运行时
Language Independence: The CLR
.NET CLR提供一个公共上下文来执行所有的.NET组件,而不考虑具体的编写语言。CLR管理代码运行时的方方面面,包括提供内存管理,安全的运行环境,以及访问底层操作系统服务。因为CLR管理着代码行为的这些方面,所以针对CLR的代码被称为托管代码。CLR提供了足够强的语言互操作能力,允许组件在开发和运行时高度交互。这是因为CLR基于一个严格的类型系统,.NET所有的语言都必须遵守该类型系统——每个.NET语言的所有构造(如类、接口、结构和基本类型)都必须编译成CLR兼容类型。但是,这种语言互操作性是以无法再使用Windows和COM开发人员数年来一直使用的语言和编译器为代价的。原因是,原来的编译器产生的代码不是针对CLR的,这类代码并不遵守CLR类型系统(CLR type system),因此CLR就不能管理这类代码。
为了进行.NET组件编程,你必须使用由.NET框架和Visual Studio提供的.NET语言编译器中的一个。Visual Studio的第一个版本(版本1.0,称之为Visual Studio .NET2002)提供了三种新的CLR兼容语言:C#、Visual Basic .NET和Managed C++。第二个版本(版本1.1,称之为Visual Studio.NET 2003)包含了J#(适合 .NET的Java)。Visual Studio的第三个版本(版本2.0,称之为Visual Studio 2005)提供了广泛的语言扩展,譬如C# 2005和Visual Basic 2005都支持泛型。第三方的编译器厂商针对CLR提供的其他语言种类也超过了20多种,如COBOL、Eiffel等。
中间语言和JIT编译器
Intermediate Language and the JIT Compiler
有个细节经常会困扰.NET初学者,即从高级语言(譬如C# 或者Visual Basic)到托管代码再到机器代码,是如何转换的。弄懂这个过程,是理解.NET是如何支持语言互操作性(即语言独立性的核心原则)的关键,这个过程也意味着二进制的兼容性。尽管本书不想涉及底层的实现细节,而是更多关注如何最好地使用.NET,但是对CLR代码生成过程的一个概览,有助于揭开它神秘的面纱。此外,理解.NET代码生成过程是处理一些特殊安全问题的关键(第12章中进一步论述)。
编译.NET托管代码的过程有两个阶段。第一阶段,高级代码被编译到一个叫作中间语言(IL,Intermediate Language)的语言中。IL的构造看起来更像机器代码,而不是高级语言,但IL的确包含一些抽象概念(譬如基础类和异常处理),这就是为什么这个语言被称为中间语言。IL被封装在DLL或EXE中。.NET中的EXE程序集和DLL程序集的主要区别是,虽然DLL和EXE都可被加载到一个已经运行的程序中,但只有EXE可以直接执行(以后对此会作进一步的论述)。由于计算机的CPU只能执行原始机器代码,而不是IL,所以在运行时需要进一步编译,让IL转变成真正的机器代码(第二阶段)。即时(JIT)编译器就是处理第二阶段编译过程的。
第一次编译高级代码时,高级语言编译器处理两件事情:首先它将IL保存在EXE 或DLL中,然后为每个类方法创建一个机器代码存根。该存根调用JIT编译器,并将自身方法地址作为参数传入。JIT编译器从DLL或EXE中找到对应的IL,将它编译成机器代码,然后用新生成的机器代码来取代内存中的存根。想法就是,当一个已经被编译的方法调用另一个未被编译的方法时,它实际上调用的是存根。存根首先调用JIT编译器,JIT编译器将IL代码编译成原始机器代码。然后,.NET再次调用该方法,从而真正地执行程序。从这以后,对该方法的调用,将以本地代码方式执行。应用程序对每个被调用的方法只需要编译一次,从未被调用的方法也将永不被编译(译注)。
当编译器生成一个EXE文件时,它的入口点是Main()方法。当加载器加载EXE时,会检测是否托管EXE。加载器加载.NET运行时库(包括JIT编译器),然后调用EXE的Main() IL方法。这样就会触发一次编译,即将Main()方法中的IL编译到内存中的原始代码中去,.NET应用程序就开始运行。一旦IL编译到了原始代码中,IL就能够自由调用其他的本地代码。当程序终止时,本地代码就会被删除,下一次应用程序运行时,IL将被JIT编译器编译成本地机器代码。
JIT编译提供了许多重要的好处。这点你在本章的后面会了解到,JIT编译提供给.NET开发人员滞后绑定的灵活性,同时也保证编译时类型安全。JIT编译也是二进制组件兼容的关键。此外,如果不同平台(譬如Windows XP和Linux)的.NET运行时实现都提供完全相同的标准服务的话,那么至少在理论上,.NET应用程序是可在不同平台间移植的。
你可能会担心JIT编译在性能方面的损失。然而,如果你知道由JIT编译器生成的代码比由传统的静态源代码编译器生成的代码运行得更快这一事实的时候,你可能就不会太担心这种损失了。例如,JIT编译器知道CPU的具体类型(譬如Pentium III 或 Pentium 4)之后,将充分利用该类型CPU提供的附加指令集。相比之下,传统的编译器必须生成最通用的代码,譬如386-指令集,因而无法利用新CPU的功能。JIT编译器的未来版本,也许可以跟踪一个应用程序使用代码的方式(分支指令使用的频率、前视分支等),然后重新编译,从而优化该特定的应用程序(或者甚至是一个特定的用户)使用组件的方式。JIT编译器也可基于实际可用的机器资源,譬如内存或CPU速度,来生成最优化的机器代码。注意,这些高级功能还没有被实现,但这种机制有提供所有这些功能的潜力。一般而言,JIT编译器的最优化,是在额外的编译时间和应用程序性能的提高两者之间的一个折中,它的效果取决于应用程序的调用模式和使用方法。将来,这个损失可以在应用程序安装时计算出来,或从用户的偏爱表中查询出来。
.NET编程语言
.NET Programming Languages
IL是所有.NET程序语言的共同点。至少在理论上,两种不同的语言中,同等的指令应该产生同一的IL。因此,先前用来区分编程语言的传统性能和能力的特征已经不适用于比较.NET语言了。然而,这并不意味着你选择何种语言都没有什么关系(这点稍后会讨论)。
原始映像编译
如果不想使用JIT编译,开发人员可以通过一个被称为“本地映像生成器(Ngen.exe)”的应用程序,将IL代码编译成本地代码。例如,“Windows Forms”框架是以在本地映像的方式安装的。本地映像被自动存储到一个特定的全局地点,称为本地映像缓存,是一个指定的计算机全局存储区。当程序集解析器搜寻一个程序集来加载时,程序集解析器首先从本地映像缓存中查找兼容的版本,如果找到,就使用本地映像版本。你可以在部署和安装时使用Ngen.exe来生成本地映像,从而最优化地利用每台计算机的能力。使用Ngen.exe最主要的动机是避免JIT编译器的性能损失,这可能是比较担心客户端应用程序的一个原因。你不想看到你的用户界面应用程序启动就要花一两秒时间,你也不想看到你的最终用户在第一次选择了某个菜单项或点击了某个按钮后还会等好一会儿。使用本地映像的缺点则是丧失了JIT编译的大部分优势。
Ngen.exe的另一个缺点是安装程序多了一个步骤。部署.NET应用程序可以如同复制EXE和DLL到用户计算机中一样的简单,但是要使用Ngen.exe就没那么简单了。我建议只有在仔细分析了你的情形,并且确信在你的应用程序中JIT编译器会造成性能瓶颈之后,再考虑使用Ngen.exe。检验它的最简单的方式是,同时针对JIT编译的版本和应用程序的本地映像版本运行你的分析和测试包,并对比测试结果。
事实上,所有.NET的组件不论是以何种语言写的,都在相同的托管环境下执行;每种语言中的所有指令必须编译成一个预定义的CLR兼容类型,基于这两个事实就允许了高度的语言互操作性。在一个语言中定义的类型,在其他的语言中都有同等的本地表示法。你可以使用现存的所有语言都支持的CLR类型,或者定义新的自定义类型。CLR也提供一个统一的基于异常的错误处理模式:某种语言中抛出的异常可以被另一种语言捕获和处理,你可以使用默认的CLR异常类集,或为了特定的用途导出和扩展它们。你也可以从一个语言中触发事件,并且在另一个语言中捕获它们。此外,因为CLR仅仅知道IL,所以安全权限和安全要求可以跨越语言屏障。例如,用CLR来确认调用者是否有合适的权限来使用另一个对象是完全没有问题的,即使它们两者是用不同的语言开发的。
.NET组件是语言独立的
.NET Components Are Language-Independent
正如第1章所解释的,面向组件编程的一个核心原理是语言独立性。当客户端在某个对象
上调用方法时,不应该考虑用于开发客户端或对象的编程语言,也不应该影响客户端与对象交互的能力。因为所有的.NET组件在运行以前,就被编译到IL中,从而和高级语言无关,根据定义,这样的结果就是语言独立性。在运行时,JIT编译器会链接访问组件入口点的客户端。这种语言独立性与COM支持的语言独立性类似,因为.NET开发工具可以读取元数据以及IL,.NET也提供开发时语言独立性,它允许你与用其他语言编写的组件交互或甚至导出。例如,所有的.NET框架基础类均用C# 编写,但是C# 和Visual Basic开发人员都可以使用它。
选择一种.NET语言
Choosing a .NET Language
Visual Studio 2005提供了四个CLR兼容的语言:Visual C# 2005(简称为C#)、Visual Basic 2005(简称为Visual Basic)、J#和Managed C++。Managed C++主要是为了互操作性、移植和迁移用户以及高级情形;大多数.NET开发人员不会将其作为主流语言对待。J#是为了维护和移植以前的J++应用程序的。因此,真正的问题是:作为.NET开发人员,你应该选择C#还是Visual Basic 2005呢?微软的官方回答是:所有的.NET语言在性能和使用方便上是不相上下的。因此选择一种语言应该基于个人的喜好。按照这个原则,如果你有 C++的基础,你可以很自然地选择C#;如果你有Visual Basic 6 (VB6)的基础,你可以选择Visual Basic 2005。然而,我相信仅仅基于你过去的语言经验来作决定不是最好的方法,你必须不仅知道你有什么样的基础,也应该知道自己的目标。
尽管C#和VB.NET以及它们各自的开发环境,在.NET第一个版本时几乎是相同的,但.NET的第三个版本(2.0版本)和Visual Studio 2005引进了编程模式、环境、编程体验上的主要区别。C#开创性地引进了代码重构、代码扩充、迭代、代码格式化选项和许多方便的语言性能,譬如委托推断(Delegate Inference)。Visual Basic 2005引进了My对象(本质上是全局变量的集合),像VB6一样对Main()方法的隐藏,新颖的项目选项和许多功能的内置实现,自动任务工具,所有这些都旨在提高开发人员的生产效率。Visual Basic 2005的目标是尽可能像VB6那样易用,同时允许开发人员生成尽可能快的应用程序。虽然这两种语言都引进了对泛型的支持,但泛型是C#团队的核心功能,在Visual Basic 2005产品中却只是一个后来附加物。反之,虽然两个语言都引进“编辑后继续运行(edit-and- continue)”,但这是Visual Basic 2005产品的主要功能,对C#却只是一个很晚的附加物。
随着两种语言进一步的发展来更好地服务它们不同的市场和需求,我相信这种趋势也会继续,并且在未来的.NET版本中只会进一步加强。这是因为今天的开发界大致分为开发工具的两类用户:快速应用程序开发人员和企业应用(或者系统应用)开发人员。这两类人非常不同,不仅因为他们使用的语言不同,主要是因为他们开发的应用程序类型和使用的开发方法论不同。
第一个占领市场,以及尽可能快地开发应用程序通常是以牺牲长期可维护性为代价的。此外,这些目标需要不同的技能、质量控制实践和管理心态。传统观点认为,快速应用程序开发(RAD)市场被VB6和它之前的版本所统治。就是技能相对较低的开发人员也可以快速、高效地生成应用程序。VB6在封装底层的Windows编程模式和与COM交互等方面做得不错。VB6的问题是,在应用程序的某个功能点上,你会碰到VB6的天花板——如果不借助于环境之外的工具或对VB6内部运作非常了解,有些事情你在VB6中就做不了。这个技能曲线图在图2-1中有所展示。
图2-1:开发工具的技能与功能性比较
像C++这样的语言和像MFC、COM这样的技术,它们在能力上是没有极限的,从支持多线程到面向对象建模再到窗口信息拦截,通常新功能的获得是以技能成本的增加为基础的。然而,甚至是最通常的MFC应用程序也需要相当多的技能,更为复杂的应用程序经
常是超出大部分开发人员能力范围的。.NET提供了一个重新开始的记录,相对于C++而言,实质上是降低了进入的门槛,在.NET中没有无形的最高限度:.NET的优点和编程模型与COM和MFC的类似。C++和COM开发人员喜欢.NET是因为它很方便。 VB6 的高级开发人员喜欢.NET的原因是他们不需要特别高的技能去开发所需的高级功能,他们也能直接获得增加功能的新技能。问题是,如何来处理如图2-1示范的.NET曲线和VB6曲线之间的区域。不幸的是,大部分 VB6 开发人员落在了.NET的图表一个区域内,目前.NET还提供不了他们什么东西,因为对于快速应用程序开发目的而言,他们选VB6可能更好些,而.NET则要求更高的技能和能力。
我相信微软很清楚地了解这个情形,为更好地服务它的目标受众,Visual Basic未来的版本将填补上技能/功能曲线上缺少的区域。另一方面,企业应用的生命周期大概在五到七年,使用快速应用开发节省的时间是微不足道的——真正的问题是长期维护的成本。在一个大的应用程序中使用面向组件的分析和设计方法需要大量的技能和时间,如果处理不当,就会导致不可维护的代码。而且,应用程序越大,你就会更倾向长期维护,从而最大化投资开发的收益。可维护性和可扩展性与合理的设计、架构、抽象及其组件和接口分解都有莫大关系。企业级开发人员倾向于花大量的时间关注代码本身,而不是关注向导和周边工具。对企业应用开发人员,微软推荐使用C#,它关注于代码结构和构造。一个典型的企业级应用程序的调试,往往涉及对问题的耗时分析和思索,因此C# 小组不怎么关注类似“编辑再继续”这样的功能就不觉得奇怪了。
我相信如果你是快速程序开发人员,即便你有C++/MFC的基础,Visual Basic 2005依旧是你的首选工具;如果你是企业应用开发人员,考虑到长期维护成本是压倒性的因素,即使你有VB6的基础也应该选择C#(译注)。
声明:本文转载自http://book.csdn.net/bookfiles/400/10040014719.shtml