高阶特性
支持垃圾回收对运行时的一个深远影响是所有代码都需要做额外的记录。而类型安全也有一个重要影响,即要求对程序需要从更高的层面(CIL)来描述,即字段和函数都需要有详细的类型信息。类型安全还强制CIL支持其它高阶编程语言元素,而表述这些高阶元素也需要运行时支持。这些高阶特性中最重要的两个特性用来支持面向对象编程的两个基本元素:继承和虚拟函数调度。
面向对象编程
继承是一个相对来说比较简单的机制。其基本思路是子
类型里的字段是其基类
里字段的超集,在子
类的布局里,先布局基类
里的字段,这样,需要处理指向一个基类
实例的指针的代码,即使给它传递一个实际指向子
类型实例也可以工作
。这样一来,我们就说子
类是从基
类继承而来的,即它可以用在任何需要用到基类
的代码。代码被称为 多态 是因为相同的代码可以被不同的类型使用。因为运行时需要保证类型强制,所以其需要规范继承的方式以便验证类型安全。
虚拟函数调度普及了继承的多态性。它允许基类定义可以在子类里 重写 的函数。处理基类类型变量的代码,在运行时可以期望对其虚拟函数的调用会被调度到对象实际类型里重写的函数上。虽然这种 运行时调度的逻辑 可以使用不通过CLR直接支持的原生CIL指令实现,但这样做有两个缺点:
- 这样就不类型安全了(调度表里的错误将造成灾难性的后果)。
- 每个面向对象编程语言在实现虚拟函数调度逻辑时很可能采取稍具差别的做法。这样一来,跨编程语言之间的互操作就无法实现了(即一个编程语言无法继承另一个编程语言里定义的基类)。
因为这样的原因,CLR直接内置基本的面向对象的特性。CLR尝试将继承模型“编程语言中立”,即不同的编程语言共享相同的继承层次结构。不幸的是,这个不是一直都有可能做到的。实际上,多重继承可以用很多不同的方式实现。CLR决定对于定义了字段的类型不支持多重继承,但是对少数不包含字段定义的特定类型(称作接口)支持多重继承。
特别需要注意的是,虽然CLR支持这些面向对象的理念,但是它不强制必须用它们。没有继承概念的编程语言(如函数式编程语言)就不会使用这些特性。
值类型(和装箱)
面向对象编程的一个微妙但影响深远的理念是对象身份:即使所有的字段值都相同,但对象(通过不同的分配函数创建)可以是不同这个理念。对象身份这个概念跟对象是通过引用(指针)访问而不是通过值来访问这个概念强烈有关。如果两个变量赋值了相同的对象(即指针指向相同的内存),那么更新其中一个变量会影响另一个。
然而,对象身份这个概念不是对所有类型都是一个好想法。例如,程序员一般不会将整数当作对象处理。如果数字“1”是在两个地方分配的,程序员通常希望将两个对看成是相等的,但又不希望更新一个却影响到另一个。实际上,有很多编程语言如函数式编程
索性放弃了对象身份和引用语义这些概念。
虽然有可能创建一个“纯”面向对象系统,即将所有东西(包括整数)都当作对象(Smalltalk-80就是这样做的),一些违背这一统一理念的实现“技巧”来达到更高的效率是必要的。一些编程语言(Perl, Java, Javascript)采用实用主义做法,通过值来处理一些类型(如整数),而通过引用来处理其他类型。CLR也采用这种混合模式,但与其他的不同,允许用户自定义的值类型。
值类型的关键特性如下:
- 每个局部变量,字段或者值类型数组中的元素都有独立的数据拷贝。
- 当一个变量、字段或者数组元素被赋值给另外一个变量,那么值被拷贝过去了。
- 相等是在变量里的数据中定义的(而不是位置)。
- 每个值类型有一个对应的引用类型,这个引用类型只有一个隐式的未命名的字段。其被称为装箱值。装箱后的值类型可以参与继承并且也有对象身份(虽然不建议这么做)。
值类型跟C(和C++)里的结构体非常相似。与C类似,指针可以指向值类型,但是指向类型的指针与结构体类型本身是不一样的。
异常
CLR直接支持的另一个高阶编程元素就是异常。异常是允许程序员在错误发生的时候 抛出 一个任意对象的编程特性。当一个对象被抛出后,CLR在堆栈里搜索可以 捕捉 这个异常的函数。如果找到了捕捉函数,则从此位置继续执行。异常的作用就是避免了不检查函数返回的错误值的这个常见编程错误。因为异常是帮助程序员避免犯错的(也就是编程变得更简单),所以CLR支持它也就不奇怪了。
从另一个角度说,虽然异常避免了一个常见错误(不检查错误),但是却没有阻止另一个(在错误发生的时候将数据架构恢复到稳定状态)。这也意味着,当异常被捕捉后,继续程序的执行是否会导致其他额外错误(由第一个错误引发)是很难获知的,这个CLR在将来很有可能改进的地方。当然,以目前的情况,实现了异常还是向前迈进了一大步(我们只是想继续往前)。
参数化类型(泛型)
CLR 2.0之前,可参数化的类型只有数组。其它容器(如哈希表,列表,队列等)只能操作一个通用的Object类型。不能创建类似 List<ElemT>,或Dictionary<KeyT, ValueT>对性能是有负面影响的,因为值类型在集合中必须是装箱过的,并且在读取的时候需要拆箱强制转换。即使这样,都不是CLR支持泛型的最大理由,主要因素是 泛型使得编程更简单。
这个理由是很好理解的。最简单的办法是设想所有类型都被替换成通用的Object类型后的类库。这个结果跟类似JavaScript的动态语言不一样,在这样的世界里,程序员有很多种办法编写错误(但是类型安全)的程序。函数的参数类型应该是列表?字符串?整数?还是都可以?在函数的声明中很难看出来。更糟糕的,如果函数返回一个Object实例,有哪些函数可以接受其作为参数?一般框架都有几百个函数;如果都接受Object类型作为参数,很难判断一个对象实例适合被哪些函数处理。简短来说,强类型帮助程序员更明白的表达其意图,而且也允许工具(如编译器)保证他的意图,这在效率上有很大的提升。
这些好处不仅仅因为类型可以被放进List或者Dictionary而消失。为一个的问题是泛型是应该当作一个编程语言特性,在生成CIL指令时被“编译器丢掉”,还是应该作为CLR的一等公民支持。两种实现方法都可行。CLR团队采取将泛型作为CLR内置的支持,因为如果不这样做,泛型可能被不同编程语言采用不同方法实现。这就意味着编程语言互操作会变得很麻烦。在一个类库里由泛型来表达程序员意图最具价值的地方 是在接口。如果CLR本身不支持泛型,那么类库就无法用它,而一个很重要的可用性特性就没有了。
将程序当作数据(反射API)
CLR的最重要的功能是垃圾回收,类型安全和高阶编程语言特性。这些特性强制对程序的描述在一个比较高的级别。一旦这些数据存在于运行时(在C和C++程序里不是这样的),将这些丰富的信息公开给程序员是非常有价值的。这个想法的结果就是System.Reflection里的接口(这样命名是因为允许程序检视自己)。这个接口允许你探索程序的方方面面(它的类型,继承关系,都定义了哪些函数和字段)。实际上,因为几乎没损失什么信息,为托管代码制作一个好的“反编译器”是可能的(如NET Reflector)。虽然这个功能在保护知识产权方面有些惊悚(这个可以由刻意销毁这些信息的混淆工具来弥补),但这也体现了托管代码在运行时有丰富的信息。
除了在运行时可以检视程序,还可以操控它们(如调用函数,给字段赋值等)。还有更强大的功能,即在运行时生成代码(System.Reflection.Emit)。实际上,有些函数库使用这个功能来创建匹配字符串的代码(System.Text.RegularExpressions),和创建代码将“序列化”对象保存到文件或通过网络传输。这种能力在之前是无法做到的(要不你就得写一个编译器)。在CLR的帮助下,我们可以解决很多编程问题。
虽然反射的功能很强大,但需要谨慎使用。反射通常比静态编译要慢很多。更重要的是,自引用的系统很难理解。因此Reflection和Reflection.Emit里的功能只在确定有价值的情况下才使用。
其他功能
CLR最后一组功能跟其基础架构(如GC、类型安全、高阶的规范)无关,但对于完善运行时系统很有帮助。
与非托管代码互操作
托管代码需要能调用非托管代码的功能。有两种互操作“风味”。第一个是可以直接调用那个非托管函数(这个叫平台调用或PINVOKE)。非托管代码同样有面向对象的互操作模型叫做COM(组件对象模型),比任意的函数调用有更多的结构。由于COM和CLR都有对象和其他约定(如如何处理错误,对象的生命周期等)模型,CLR跟COM的互操作在有一些特别支持的情况下可以做的更好。
提前编译
在CLR里,托管代码是以CIL指令,而不是原生指令呈现的。转换成原生指令的过程发生在运行时。作为一个优化,从CIL里转换的原生代码可以用一个称为crossgen(类似.NET Framework里的ngen)工具保存在文件里。这样可以节省运行时很多编译的时间,当类库很大的时候这个就变得更重要了。
线程
CLR早就预见需要在托管代码里支持多线程的必要性。从一开始,CLR函数库里就有System.Threading.Thread这个类,作为操作系统里线程的1对1封装。然而,因此其仅是操作系统线程的简单封装,创建一个System.Threading.Thread相对来说比较昂贵(需要几个毫秒来创建)。在大多数情况这都可以接受。在只处理很小工作量(只有几十毫秒),这个在服务器端代码很常见(每个任务处理一个web页面请求),或者需要利用多核特性的代码(如多核排序算法)。为了支持这种情况,CLR提供了线程池来给工作任务排队。在这种情况下,CLR负责创建必要的线程来完成工作。虽然CLR通过System.Threading.Threadpool类型来直接公开线程池,更推荐的方式是采用Task Parallel Library,其对常见的并行控制添加了额外的支持。
从实现的角度来看,线程池的一个重要创新是其负责保证一个优化数量的线程来负责完成工作。CLR使用一个反馈系统来监控线程数和生产率,并调整线程的数量以最大化生产率。这样程序只需要考虑是否需要“并行”(也就是创建工作任务),而不是需要多少并行量(取决于工作量和运行程序的硬件)这样更基础的问题。
结论和资源
呼!CLR做了不少!仅仅是描述CLR的一些功能就用了不少页码,还没有开始讨论起内部实现机制。希望这个介绍能给你对内部实现细节的更深入的理解。框架的基本结构是:
- CLR是一个支持很多编程语言的框架
- CLR的目标是使编程更简单
- CLR的基础功能是:
- 垃圾回收
- 内存和类型安全
- 支持高阶编程语言的特性