公共语言规范
Common Language Specification (CLS)与CTS一起以保证语言互操作性的实现。CLS是一个最小的标准集合,所有以.NET作为目标运行平台的编译器都必须遵守。因为IL是一个内涵丰富的语言,许多编译器的作者都喜欢实现IL 和 CTS所提供的功能限制的一个子集。这样也好,只要编译器支持CLS所支持的所有定义就好。
提示:你完全可以编写与CLS不兼容的代码。但是,如果你这样做,编译出来的IL代码就无法保证完全符合语言互操作性。
比如,让我们来看一个关于字母大小写的例子。IL是区分大小写的。用区分大小写的语言来编写程序的开发人员习惯利用这种区分大小写的灵活性来给他们的变量命名。但是,Visual Basic .NET却是不区分大小写的。CLS的工作就是使CLS兼容代码明白,他们不能使用任何两个仅仅在大小写上有所区别的名称。这样,Visual Basic .NET就可以和任何CLS兼容代码协同工作了。
这个例子说明了CLS有两种工作方式。首先,各个编译器功能不必非要强大到满足.NET的所有特性,这就鼓励了其他的编译器厂商将自己的语言加入到.NET这个大家庭来。第二,他也提供了这样的一个保证:如果你限制你的类使他满足CLS的兼容特性,那么其他的兼容语言编写的代码都可以使用你的类。
这种方法的优点是使用CLS兼容特性的限制只适用于公共类和类的公有或者受保护成员。而对于那些你的类中的私有成员的实现,你完全可以编写任何非CLS兼容代码,因为其他的程序集的代码是无论如何也无法访问这部分的。
在这里我们不需要详细的讨论CLS规范的细节。一般而言,因为C#中的非CLS兼容特性很少,所以CLS对你的C#代码影响不大。
1.3.3.2 垃圾收集
垃圾收集器是.NET的内存管理方法,特别是针对如何回收运行的应用程序申请的内存的这个问题,垃圾收集器提供了一个好地解决办法。至今为止,针对如何释放进程动态申请的系统内存这个问题,在Windows平台上已经出现了两种技术。
? 让应用程序代码自己手工实现内存释放
? 让对象为呼应用数目
让应用程序代码来负责释放内存是低级,但是高性能的语言采用的方法,比如C++。这种方法是高效的,他的优点是资源一旦不再需要就立即释放(一般情况下)。但是,他也带来了很大的缺点,由此而引发缺陷非常频繁。代码申请的内存当他们不再需要时当然应该明确的告知系统。但是,非常容易就忽略这一点,从来导致了内存泄漏。
尽管现代开发环境确实提供了帮助探测内存泄漏的工具,但是因为此类问题只有到大量的内存泄漏以至于Windows拒绝批准进程申请内存时才会被发现,所以仍然有难以捕捉到的bug。从这一点上来说,由于对内存的需求,整个计算机就会逐渐的变的相当慢。
COM采用维护应用计数的方法来解决这个问题。COM的方法是每个COM组件都维护一个计数器,这个计数器保存的是当前有多少客户机应用自己。当这个计数器的数值下降到零时,组件就会销毁自己并且释放相关的内存和资源。但是问题时COM的这种解决办法仍然是依靠客户机及时地通知组件他们已经使用引用完毕。只要有一个客户机没有及时销毁自己,那么组件就会留在内存中。但是从某种程度上讲,这种内存泄漏或许比C++的内存泄漏更为严重,因为COM对象会保存在自己的进程中而永远不会被系统删除(至少对于C++内存泄漏问题,系统可以在进程停止时重新收回所有的内存资源)。
现在,.NET运行时改成依靠垃圾收集器来回收资源了。垃圾收集器就是一个专门用来整理内存的程序。方法是将所有动态申请的内存都分配到堆上(这对所有的语言都一样,但是在.NET中,CLR要单独维护自己管理的堆以供.NET应用程序使用)。情况常常是这样的,当.NET检测到分配给一个进程的托管堆已经变满因而需要整理时,他就会调用垃圾收集器。垃圾收集器就会扫描你的代码中的变量,检查托管堆上的对象的引用,以确定哪些个是现在还在使用的——也就是说哪些对象还有对自己的引用。任何已经没有你的代码引用的对象都会被认为是不再需要使用的,因此他们都将被移出。Java也有一个类似的垃圾收集器。
.NET中的垃圾收集器本身就是为了适应进程的工作而设计的。其原则是:除非你复制一个已经存在的引用,否则你不可以为已经存在的对象建立引用,并且IL是类型安全的。这里的意思就是说,如果存在一个对对象的引用,那么我们就有足够的信息以明确的确定对象的类型。
垃圾收集机制就不适合用在象非托管C++这样语言上,例如,C++允许指针在不同的类型中自由的转换。
垃圾收集机制的一个重要的特点是它的操作时间是不确定的。换句话说,你不能保证垃圾收集器是何时被调用的,当CLR觉得有必要调用它的时候他就会被调用(除非你明确调用它)。尽管你也可以不考虑这些过程而直接在你的代码中调用垃圾收集器。
1.3.3.3 安全性
因为.NET能够提供基于代码的安全性机制,所以.NET在安全性机制方面是优于Windows的,更何况Windows仅仅提供了基于角色的安全性。
基于角色的安全性机制是基于识别进程所运行的帐户的基础上的,换句话说,是谁拥有并运行这个进程?另一方面,寄予代码的安全性是建立在识别代码本身是如何执行以及其本身的可信度有多大的基础上的。幸亏IL提供了强大的的类型安全机制,CLR才能够在代码运行之前就预先检查代码的安全性以确定需要的安全许可。.NET也提供了一种机制,代码可以预先指出它需要运行在什么样的安全许可下。
基于代码的安全性的重要性是她可以降低运行来源不明的代码所冒的风险(比如你在Internet所辖在德代码)。例如,即便是代码是运行在管理员帐户下,基于代码的安全性仍然可以指出这段代码仍然不应该被允许执行某些管理员通常应该被允许的操作,例如读或写环境变量,读或写注册表,或者访问.NET反射特性。
安全性的详细问题我们将在14章讨论。
1.3.3.4 应用程序域
应用程序域是.NET的一项重要改革。运行的应用程序既需要彼此隔离又需要可以彼此通讯,像这样一个棘手的问题,应用程序域就是设计来解决他的。这方面经典的案例是一个Web应用服务器同时需要相应许多的游览器的请求。因此,应用服务器可能就同时为了给不同的请求服务而运行许多的不同的实例。
在.NET出现之前,有两种方法可以解决这个问题。一个是允许这样实例共享同一个进程,但是这样可能导致出现一个实例失败就会使整个Web服务器关闭的现象。另一个方法是将这些实例彼此孤立在不同的进程中,但是这样也带来了性能的降低。
到目前为止,我们是通过将代码运行在不同的进程中来实现代码隔离的。当你启动一个应用程序的时候,他是在一个进程的环境中运行的。Windows系统通过地址空间来彼此隔离进程。具体的方法是每个进程都有4GB的虚拟内存用来存放他自己的数据和可执行代码(对32位系统来说是4GB,对64位的系统就会有更多)。Windows通过将虚拟内存与实际的物理内存或者磁盘空间建立映射关系来间接扩展虚拟内存的大小。每个进程都有不同的映射,而且虚拟地址空间块映射的德实际内存都是不会相互重叠的(如图1-2所示)。
图1-2
一般情况下,进城只能通过虚拟内存中的特定地址访问物理内存,进程是不可以直接访问物理内存的。这样一个进程要想访问其他进程的空间就是不可能的了。这样也就保证了任何有错误行为的代码都不能损害自己地址空间之外的任何东西(注意,在Windows 95/98上,这种安全措施还不是象在Windows NT/2000/XP/2003平台上那样彻底,所以理论上还存在应用程序写入不适当的内存而导致Windows崩溃的可能性)。
引入进程的目的不仅仅是用来彼此隔离运行代码的实例。在Windows NT/2000/XP/2003系统中,进程还是安全权限和许可的分配单元。每一个进程都有自己的安全标志,这个标志告诉Windows什么样的操作是允许这个进程执行的。
虽然进程对于安全性有巨大的帮助,但是它的最大缺点就是性能的损失。通常实际情况下许多进程需要在一起工作,因此他们就需要彼此通信。一个明显的例子是,有一个进程需要调用一个COM组件,而这个COM组件也是可以执行的,所以COM组件需要运行在自己进程中。在COM中使用代理也会有类似情况发生。由于进程之间是不共享内存的,所以一个复杂的工作就是在进程之间复制数据。这样做的结果就是带来了巨大的性能损失。如果你希望使组件协同工作而不希望有性能的损失,那你就必须使用基于DLL的组件技术,这样所有的代码都会运行在同样的地址空间里——当然,这样带来的风险就是,如果其中的一个组件运行出错,那么这个进程中所有的程序都会关闭。
设计应用程序域的目的就是要在对性能毫无损失的情况下解决进程间共享数据的问题。具体的做法是,每一个进程都被划分到一个应用程序域中。每个应用程序域大体上对应于隐格单独的应用程序,每个执行的线程都运行在一个特定的应用程序域中(如图1-3所示)。
图1-3