17xie > C# 2005 & .NET 3.0高级编程(第5版) > 1.3 详细介绍中间语言
背景:                 
[本书目录] [图书首页] [本书讨论区]  
链接地址:http://www.17xie.com/read-58370.html    注册17xie 一起来写书 实现您的出书梦想!

1.3  详细介绍中间语言

通过前面的学习,我们理解了Microsoft中间语言显然在.NET Framework中有非常重要的作用。C#开发人员应明白,C#代码在执行前要编译为中间语言(实际上,C#编译器仅编译为托管代码),这是有意义的,现在应详细讨论一下IL的主要特征,因为面向.NET的所有语言在逻辑上都需要支持IL的主要特征。

下面就是中间语言的主要特征:

       面向对象和使用接口

       值类型和引用类型之间的巨大差别

       强数据类型

       使用异常来处理错误

       使用特性(attribute)

下面详细讨论这些特征。

1.3.1  面向对象和接口的支持

.NET的语言无关性还有一些实际的限制。中间语言在设计时就打算实现某些特殊的编程方法,这表示面向它的语言必须与编程方法兼容,MicrosoftIL选择的特定道路是传统的面向对象的编程,带有类的单一继承性。

注意:

不熟悉面向对象功能概念的读者应参考附录A,获得更多的信息。附录A可以从www.wrox. com上下载。

除了传统的面向对象编程外,中间语言还引入了接口的概念,它们显示了在带有COMWindows下的第一个实现方式。.NET接口与COM接口不同,它们不需要支持任何COM基础结构,例如,它们不是派生自IUnknown,也没有对应的GUID。但它们与COM接口共享下述理念:提供一个契约,实现给定接口的类必须提供该接口指定的方法和属性的实现方式。

前面介绍了使用.NET意味着要编译为中间语言,即需要使用传统的面向对象的方法来编程。但这并不能提供语言的互操作性。毕竟,C++Java都使用相同的面向对象的范型,但它们仍不是可交互操作的语言。下面需要详细探讨一下语言互操作性的概念。

首先,需要确定一下语言互操作性的含义。毕竟,COM允许以不同语言编写的组件一起工作,即可以调用彼此的方法。这就足够了吗?COM是一个二进制标准,允许组件实例化其他组件,调用它们的方法或属性,而无需考虑编写相关组件的语言。但为了实现这个功能,每个对象都必须通过COM运行库来实例化,通过接口来访问。根据相关组件的线程模型,不同线程上内存空间和运行组件之间要编组数据,这还可能造成很大的性能损失。在极端情况下,组件保存为可执行文件,而不是DLL文件,还必须创建单独的进程来运行它们。重要的是组件要能与其他组件通信,但仅通过COM运行库进行通信。无论COM是用于允许使用不同语言的组件直接彼此通信,或者创建彼此的实例,系统都把COM作为中间件来处理。不仅如此,COM结构还不允许利用继承实现,即它丧失了面向对象编程的许多优势。

一个相关的问题是,在调试时,仍必须单独调试用不同语言编写的组件。这样就不可能在调试器上调试不同语言的代码了。语言互操作性的真正含义是用一种语言编写的类应能直接与用另一种语言编写的类通信。特别是:

       用一种语言编写的类应能继承用另一种语言编写的类。

       一个类应能包含另一个类的实例,而不管它们是使用什么语言编写的。

       一个对象应能直接调用用其他语言编写的另一个对象的方法。

       对象(或对象的引用)应能在方法之间传递。

       在不同的语言之间调用方法时,应能在调试器中调试这些方法调用,即调试不同语言编写的源代码。

这是一个雄心勃勃的目标,但令人惊讶的是,.NET和中间语言已经实现了这个目标。在调试器上调试方法时,Visual Studio 2005 IDE提供了这样的工具(不是CLR提供的)

1.3.2  相异值类型和引用类型

与其他编程语言一样,中间语言提供了许多预定义的基本数据类型。它的一个特性是值类型和引用类型有明显的区别。对于值类型,变量直接保存其数据,而对于引用类型,变量仅保存地址,对应的数据可以在该地址中找到。

C++中,引用类型类似于通过指针来访问变量,而在Visual Basic中,与引用类型最相似的是对象,Visual Basic 6是通过引用来访问对象。中间语言也有数据存储的规范:引用类型的实例总是存储在一个名为托管堆的内存区域中,值类型一般存储在堆栈中(但如果值类型在引用类型中声明为字段,它们就内联存储在堆中)。第2章“C#基础”讨论堆栈和堆,及其工作原理。

1.3.3  数据类型

中间语言的一个重要方面是它基于强数据类型。所有的变量都清晰地标记为属于某个特定数据类型(在中间语言中没有Visual Basic和脚本语言的Variant数据类型)。特别是中间语言一般不允许对模糊的数据类型执行任何操作。

例如,Visual Basic 6开发人员习惯于传递变量,而无需考虑它们的类型,因为Visual Basic6会自动进行所需的类型转换。C++开发人员习惯于在不同类型之间转换指针类型。执行这类操作将大大提高性能,但破坏了类型的安全性。因此,这类操作只能在某些编译为托管代码的语言中的特殊情况下进行。确实,指针(相对于引用)只能在标记了的C#代码块中使用,但在Visual Basic中不能使用(但一般在托管C++中允许使用)。在代码中使用指针会立即导致CLR提供的内存类型安全性检查失败。

注意,一些与.NET兼容的语言,例如Visual Basic 2005,在类型化方面上要求仍比较松,但这是可以的,因为编译器在后台确保在生成的IL上强制类型安全。

尽管强迫实现类型的安全性最初会降低性能,但在许多情况下,我们从.NET提供的、依赖于类型安全的服务中获得的好处更多。这些服务包括:

       语言的互操作性

       垃圾收集

       安全性

       应用程序域

下面讨论强数据类型化对这些.NET特性非常重要的原因。

1. 语言互操作性中强数据类型的重要性

如果类派生其他类,或包含其他类的实例,它就需要知道其他类使用的所有数据类型,这就是强数据类型非常重要的原因。实际上,过去没有任何系统指定这些信息,从而成为语言继承和交互操作的真正障碍。这类信息不只是在一个标准的可执行文件或DLL中出现。

假定Visual Basic 2005类中的一个方法被定义为返回一个整型——Visual Basic 2005可以使用的标准数据类型之一。但C#没有该名称的数据类型。显然,我们只能从该类中派生,再使用这个方法,如果编译器知道如何把Visual Basic 2005的整型类型映射为C#定义的某种已知类型,就可以在C#代码中使用返回的类型。这个问题在.NET中是如何解决的?

(1) 通用类型系统(CTS)

这个数据类型问题在.NET中使用通用类型系统(CTS)得到了解决。CTS定义了可以在中间语言中使用的预定义数据类型,所有面向.NET Framework的语言都可以生成最终基于这些类型的编译代码。

例如,Visual Basic 2005的整型实际上是一个32位有符号的整数,它实际映射为中间语言类型Int32。因此在中间语言代码中就指定这种数据类型。C#编译器可以使用这种类型,所以就不会有问题了。在源代码中,C#用关键字int来表示Int32,所以编译器就认为Visual Basic 2005方法返回一个int类型的值。

通用类型系统不仅指定了基本数据类型,还定义了一个内容丰富的类型层次结构,其中包含设计合理的位置,在这些位置上,代码允许定义它自己的类型。通用类型系统的层次结构反映了中间语言的单一继承的面向对象的方法,如图1-1所示。

  1-1

这个树形结构中的类型说明如表1-1所示。

  1-1

   

   

Type

代表任何类型的基类

Value Type

代表任何值类型的基类

Reference Types

通过引用来访问,且存储在堆中的任何数据类型

Built-in Value Types

包含大多数标准基本类型,可以表示数字、Boolean值或字符

Enumerations

枚举值的集合

User-defined Value Types

在源代码中定义,且保存为值类型的数据类型。在C#中,它表示结构

Interface Types

接口

Pointer Types

指针

Self-describing Types

为垃圾回收器提供对它们本身有益的信息的数据类型(参见下一节)

Arrays

包含对象数组的类型

Class Types

可自我描述的类型,但不是数组

Delegates

用于把引用包含在方法中的类型

User-definedReference Types

在源代码中定义,且保存为引用类型的数据类型。在C#中,它表示类

Boxed Value Types

值类型,临时打包放在一个引用中,以便于存储在堆中

 

这里没有列出内置的所有值类型,因为第3章将详细介绍它们。在C#中,编译器识别的每个预定义类型都映射为一个IL内置类型。这与Visual Basic 2005是一样的。

(2) 公共语言规范(CLS)

公共语言规范(Common Language SpecificationCLS)和通用类型系统一起确保语言的互操作性。CLS是一个最低标准集,所有面向.NET的编译器都必须支持它。因为IL是一种内涵非常丰富的语言,大多数编译器的编写人员有可能把给定编译器的功能限制为只支持ILCLS提供的一部分特性。只要编译器支持已在CLS中定义的内容,这就是很不错的。

提示:

编写非CLS兼容代码应该是完全可以接受的,只是编写了这种代码后,就不能保证编译好的IL代码完全支持语言的互操作性。

下面的一个例子是有关区分大小写字母的。IL是区分大小写的语言。使用这些语言的开发人员常常利用区分大小写所提供的灵活性来选择变量名。但Visual Basic 2005是不区分大小写的语言。CLS就要指定CLS兼容代码不使用任何只根据大小写来区分的名称。因此,Visual Basic 2005代码可以与CLS兼容代码一起使用。

这个例子说明了CLS的两种工作方式。首先是各个编译器的功能不必强大到支持.NET的所有功能,这将鼓励人们为其他面向.NET的编程语言开发编译器。第二,它提供如下保证:如果限制类只能使用CLS兼容的特性,就要保证用其他语言编写的代码可以使用这个类。

这种方法的优点是使用CLS兼容特性的限制只适用于公共和受保护的类成员和公共类。在类的私有实现方式中,可以编写非CLS代码,因为其他程序集(托管代码的单元,参见本章后面的内容)中的代码不能访问这部分代码。

这里不深入讨论CLS规范。在一般情况下,CLSC#代码的影响不会太大,因为C#中的非CLS兼容特性非常少。

2. 垃圾收集

垃圾收集器用来在.NET中进行内存管理,特别是它可以恢复正在运行中的应用程序需要的内存。到目前为止,Windows平台已经使用了两种技术来释放进程向系统动态请求的内存:

       完全以手工方式使应用程序代码完成这些工作。

       让对象维护引用计数。

让应用程序代码负责释放内存是低级、高性能的语言使用的技术,例如C++。这种技术很有效,且可以让资源在不需要时就释放(一般情况下),但其最大的缺点是频繁出现错误。请求内存的代码还必须明确通知系统它什么时候不再需要该内存。但这是很容易被遗漏的,从而导致内存泄漏。

尽管现代的开发环境提供了帮助检测内存泄漏的工具,但它们很难跟踪错误,因为直到内存已大量泄漏从而使Windows拒绝为进程提供资源时,它们才会发挥作用。到那个时候,由于对内存的需求,会使整个计算机变得相当慢。

维护引用计数是COM对象采用的一种技术,其方法是每个COM组件都保留一个计数,记录客户机目前对它的引用数。当这个计数下降到0时,组件就会删除自己,并释放相应的内存和资源。它带来的问题是仍需要客户机通知组件它们已经完成了内存的使用。只要有一个客户机没有这么做,对象就仍驻留在内存中。在某些方面,这是比C++内存泄漏更为严重的问题,因为COM对象可能存在于它自己的进程中,从来不会被系统删除(C++内存泄漏问题上,系统至少可以在进程中断时释放所有的内存)

.NET运行库采用的方法是垃圾收集器,这是一个程序,其目的是清理内存,方法是所有动态请求的内存都分配到堆上(这对所有的语言都一样,但在.NET中,CLR维护它自己的托管堆,以供.NET应用程序使用),当.NET检测到给定进程的托管堆已满,需要清理时,就调用垃圾收集器。垃圾收集器处理目前代码中的所有变量,检查对存储在托管堆上的对象的引用,确定哪些对象可以从代码中访问—— 即哪些对象有引用。没有引用的对象就不能再从代码中访问,因而被删除。Java就使用与此类似的垃圾收集系统。

之所以在.NET中使用垃圾收集器,是因为中间语言已用来处理进程。其规则要求,第一,不能引用已有的对象,除非复制已有的引用。第二,中间语言是类型安全的语言。在这里,其含义是如果存在对对象的任何引用,该引用中就有足够的信息来确定对象的类型。

垃圾收集器机制不能和诸如非托管C++这样的语言一起使用,因为C++允许指针自由地转换数据类型。

垃圾收集器的一个重要方面是它是不确定的。换言之,不能保证什么时候会调用垃圾收集器:.NET运行库决定需要它时,就可以调用它(除非明确调用垃圾收集器)。但可以重写这个过程,在代码中调用垃圾收集器。

3. 安全性

.NET很好地补足了Windows提供的安全机制,因为它提供的安全机制是基于代码的安全性,而Windows仅提供了基于角色的安全性。

基于角色的安全性建立在运行进程的账户的身份基础上,换言之,就是谁拥有和运行进程。另一方面,基于代码的安全性建立在代码实际执行的任务和代码的可信程度上。由于中间语言提供了强大的类型安全性,所以CLR可以在运行代码前检查它,以确定是否有需要的安全权限。.NET还提供了一种机制,可以在运行代码前指定代码需要什么安全权限。

基于代码的安全性非常重要,原因是它降低了运行怀疑其出处的代码的风险(例如代码是从Internet上下载来的)。即使代码运行在管理员账户下,也有可能使用基于代码的安全性,来确定这段代码是否仍不能执行管理员账户一般允许执行的某些类型的操作,例如读写环境变量、读写注册表或访问.NET反射特性。

安全问题详见本书后面的第16章。

4. 应用程序域

应用程序域是.NET中的一个重要技术改进,它用于减少运行应用程序的系统开销,这些应用程序需要与其他程序分离开来,但同时还需要彼此通信。典型的例子是Web服务器应用程序,它需要同时响应许多浏览器请求。因此,要有许多组件实例同时响应这些同时运行的请求。

.NET没有开发出来前,可以让这些实例共享同一个进程,但此时一个运行的实例就有可能导致整个网站的崩溃;也可以把这些实例孤立在不同的进程中,但这样做会增加相关性能的系统开销。

到现在为止,孤立代码的惟一方式是通过进程来实现的。在运行一个新的应用程序时,它会在一个进程环境内运行。Windows通过地址空间把进程分隔开来。这样,每个进程有4GB的虚拟内存来存储其数据和可执行代码(4GB对应于32位系统,64位系统要用更多的内存)Windows利用额外的间接方式把这些虚拟内存映射到物理内存或磁盘空间的一个特殊区域中,每个进程都会有不同的映射,虚拟地址空间块映射的物理内存之间不能有重叠,这种情况如图1-2所示。

  1-2

在一般情况下,任何进程都只能通过指定虚拟内存中的一个地址来访问内存——即进程不能直接访问物理内存,因此一个进程不可能访问分配给另一个进程的内存。这样就可以确保任何执行出错的代码不会损害其地址空间以外的数据(注意在Windows 95/98上,这些保护措施不像在Windows NT/2000/XP/2003上那样强大,所以理论上存在应用程序因写入不对应的内存而导致Windows崩溃的可能性)

进程不仅是运行代码的实例相互隔离的一种方式,在Windows NT/2000/XP/2003系统上,它们还可以构成分配了安全权限和许可的单元。每个进程都有自己的安全标识,明确地表示Windows允许该进程可以执行的操作。

进程对确保安全有很大的帮助,而它们的一大缺点是性能。许多进程常常在一起工作,因此需要相互通信。一个常见的例子是进程调用一个COM组件,而该COM组件是可执行的,因此需要在它自己的进程上运行。在COM中使用代理时也会发生类似的情况。因为进程不能共享任何内存,所以必须使用一个复杂的编组过程在进程之间复制数据。这对性能有非常大的影响。如果需要使组件一起工作,但不希望性能有损失,惟一的方法是使用基于DLL的组件,让所有的组件在同一个地址空间中运行—— 其风险是执行出错的组件会影响其他组件。

应用程序域是分离组件的一种方式,它不会导致因在进程之间传送数据而产生的性能问题。其方法是把任何一个进程分解到多个应用程序域中,每个应用程序域大致对应一个应用程序,执行的每个线程都运行在一个具体的应用程序域中,如图1-3所示。

  1-3

如果不同的可执行文件都运行在同一个进程空间中,显然它们就能轻松地共享数据,因为理论上它们可以直接访问彼此的数据。虽然在理论上这是可以实现的,但是CLR会检查每个正在运行的应用程序的代码,以确保这些代码不偏离它自己的数据区域,保证不发生直接访问其他进程的数据的情况。这初看起来是不可能的,如何告诉程序要做什么工作,而又不真正运    行它?

实际上,这么做通常是可能的,因为中间语言拥有强大的类型安全功能。在大多数情况下,除非代码明确使用不安全的特性,例如指针,否则它使用的数据类型可以确保内存不会被错误地访问。例如,.NET数组类型执行边界检查,以禁止执行超出边界的数组操作。如果运行的应用程序的确需要与运行在不同应用程序域中的其他应用程序通信或共享数据,就必须调用.NET的远程服务。

被验证不能访问超出其应用程序域的数据(而不是通过明确的远程机制)的代码就是内存类型安全的代码,这种代码与运行在同一个进程中但应用程序域不同的类型安全代码一起运行是安全的。

1.3.4  通过异常方法处理错误

.NET Framework可以根据异常使用相同的机制处理错误情况,这与JavaC++是一样的。C++开发人员应注意到,由于IL有非常强大的类型系统,所以在IL中以C++的方式使用异常不会带来相关的性能问题。另外,.NETC#也支持finally块,这是许多C++开发人员长久以来的愿望。

12章会详细讨论异常。简要地说,代码的某些领域被看作是异常处理程序例程,每个例程都能处理某种特殊的错误情况(例如,找不到文件,或拒绝执行某些操作的许可)。这些条件可以定义得很宽或很窄。异常结构确保在发生错误情况时,执行进程立即跳到异常处理程序例程上,处理错误情况。

异常处理的结构还提供了一种方便的方式,当对象包含错误情况的准确信息时,该对象就可以传送给错误处理例程。这个对象包括给用户提供的相应信息和在代码的什么地方检测到错误的确切信息。

大多数异常处理结构,包括异常发生时的程序流控制,都是由高级语言处理的,例如C#Visual Basic 2005C++,任何中间语言命令都不支持它。例如,C#使用try{}catch{} finally{}代码块来处理它,详见第12章。

.NET提供了一种基础结构,让面向.NET的编译器支持异常处理。特别是它提供了一组.NET类来表示异常,语言的互操作性则允许错误处理代码处理被抛出的异常对象,无论错误处理代码使用什么语言编写,都是这样。语言的无关性没有体现在C++Java的异常处理中,但在COM的错误处理机制中有一定限度的体现。COM的错误处理机制包括从方法中返回错误代码以及传递错误对象。在不同的语言中,异常的处理是一致的,这是多语言开发的重要一环。

1.3.5  特性的使用

特性(attribute)是使用C++编写COM组件的开发人员很熟悉的一个功能(使用MicrosoftCOM接口定义语言(Interface Definition LanguageIDL))。特性最初是为了在程序中提供与某些项相关的额外信息,以供编译器使用。

.NET支持特性,因此现在C++C#Visual Basic 2005也支持特性。但在.NET中,对特性的革新是建立了一个机制,通过该机制可以在源代码中定义自己的特性。这些用户定义的特性将和对应数据类型或方法的元数据放在一起,这对于文档说明书十分有用,它们和反射技术一起使用,以根据特性执行编程任务。另外,与.NET的语言无关性的基本原理一样,特性也可以在一种语言的源代码中定义,而被用另一种语言编写的代码读取。

本书的第11章详细介绍了特性。


字数:9375    最后更新:7个月以前 [04-10 20:46]happyskynet 修改
本页编辑者:happyskynet  
[前一页]:1.2 公共语言运行库  [后一页]:1.4 程序集
[在本页中加入书签] [收藏本书] [推荐本书]
  17xie论坛 > 本书讨论区 > 本页评论   (共0条)
发表评论

用户名称 匿名发表
评论内容
验证码

关于我们 | 版权声明 | 免责声明 | 诚聘英才 | 联系我们 | 合作伙伴 | 友情链接 | 广告合作 | 提交意见
Copyright © 2007 17xie.com 互联网协同写书平台 京ICP备08002671号