原 Introduction to the

公共语言运行库 (clr) 简介 === By Vance Morrison ([@vancem](https://github.com/vancem)) - 2007

什么是公共语言运行库 (clr)?简要概括如下:

>公共语言运行时 (clr) 是一个完整支持高级语言特性的虚拟机, 旨在支持各种编程语言以及它们之间的互操作。

这样说可能不是那么明了。但是确实有意义, 因为它是理解这个大型复杂的软件(称为 [clr] [clr])多种功能特性的第一步。也让读者对运行时的目的和特定有了概要的了解,在高层次上理解了运 行此,就不回迷失在具体组件的实现细节上面,在高层次上理解了运行此,就不回迷失在具体组件的实现细节上面

#一个 (非常罕见) 完整的编程平台

每个程序都和运行环境有极大的依赖性,最明显的是,程序是有一种特定的编程语言实现的,这仅仅是写程序时的第一个假设,所有的上规模的系统,都会需要一下运行库来和其他机器资源打交道(例如用户输入,磁盘文件,网络传输,等等)。程序也要能够转换成特定硬件环境能够识别的形式。程序的以来时如此广泛深入以至于程序语言常常遵循一些特定的标准来实现。例如 c++没有制定特定的c++执行文件格式,每个c++编译器都制定特定的平台架构(x86)和操作系统环境 (例如, windows、linux 或 mac os), 它描述了可执行文件格式的格式, 并指定了它将如何加载。因此,开发者不是生成 "c++ 可执行文件", 而是 "windows X86 可执行文件" 或 "pc mac os 可执行文件"。

虽然利用现有的硬件和操作系统标准通常是一件好事, 但它的缺点是将规范与现有标准的抽象级别捆绑在一起。 例如, 今天没有一个通用的操作系统具有垃圾回收堆的概念。 那么就没有办法使用现有的标准来描述一个利用垃圾回收的接口 (例如, 来回传递字符串, 而不担心谁负责删除它们)。 类似地, 一个典型的可执行文件格式提供了足够的信息来运行程序, 但没有足够的信息让编译器将其他库绑定到执行文件。 例如, c++ 程序通常使用标准库 (在 windows 上称为 msvcrt.dll), 其中包含大多数常见功能 (如 printf), 但仅此库的存在是不够的。 如果没有匹配的头文件 (例如, stdio), 程序员就不能使用该库。 因此, 现有的可执行文件格式标准既不能用来描述可以运行的文件格式, 也不能指定使程序完成所需的其他信息或运行库。

clr 通过定义一个 [非常完整的规范][ecma 规范] (ecma 标准化) 来解决这些问题, 其中包含一个程序的完整生命周期所需的详细信息, 从创建,绑定到部署和执行。 clr 指定: -GC:具有自己的指令集 (称为公共中间语言 (cil)) 的虚拟机, 用于指定程序执行的基本操作。 这意味着 clr 不依赖于特定类型的 cpu。 -元数据包含丰富的信息用于程序声明 (如类型、字段、方法等), 编译器可以通过这些外部调用信息生成生成其他可执行文件。 -CLR规定了可执行文件的格式,因此CLR不依赖已某个特定的操作系统或系统硬件。 -在程序的生命周期里, 一个 clr.exe 文件可以引用另一个 clr.exe, 也定义了在运行时如何查找被引用文件的规则。 -基于CLR特性(垃圾回收,异常,泛型)编写的类库能够使用基本功能 (如整数、字符串、数组、列表或字典) 以及操作系统服务 (例如, 文件、网络或用户交互)。

多语言支持 ----------------------

定义、说明和实现所有这些细节是一个巨大的工程, 这就是为什么像 clr 这样的完整抽象非常少见。 绝大多数的这种相当完整的抽象都是为单一语言而建的。 例如, java 运行时(jvm也是支持多语言的)、perl 解释器或 visual basic 运行时的早期版本提供了类似的完全抽象边界。 clr 与这些早期努力的区别在于它的多语言特性。 VB可能是个例外 (因为它利用了 com 对象模型), 语言内的体验通常非常好, 但是与其他语言编写的程序进行交互是非常困难的。 互操作很困难, 因为这些语言只能使用操作系统提供的基元与 "外部" 语言进行通信。 由于 os 抽象级别非常低 (例如, 操作系统没有垃圾回收堆的概念), 因此需要一些不必要的复杂技术。 通过提供公共语言运行库, clr 允许语言通过高级构造 (如来及回收的结构) 彼此通信, 从而大大减轻了互操作负担。

因为运行时是在多种语言之间共享的, 因此需要投入更多的资源来支持它。 为一种语言构建良好的调试器和分析器是一项很大的工作, 只有最重要的编程语言才有这些完备的功能。 但是, 由于在 clr 上实现的语言可以重用此基础结构, 因此对任何特定语言的负担大大减少。 更重要的是, 在 clr 上构建的任何语言都可以立即访问所有基于构建的类库。 这种大型 (和不断增长的) 功能 (调试和支持)是 clr 如此成功的一个巨大原因。

简而言之, 在CLR中,你必须放入一个文件来创建并运行程序(a complete specification of the exact bits?)。 运行这些文件的虚拟机处于高级别, 适合于实现广泛的编程语言类。 这个虚拟机,以及在该虚拟机上运行的不断增长的类库, 就是我们所说的公共语言运行时 (clr)。

#clr 的主要目标

既然我们已经基本了解了 clr 的含义, 那么稍微回顾一下并了解运行时要解决的问题。 在非常高的级别上, 运行时只有一个目标:

>CLR的目标是简化编程。

制定这个目标的原因有二:首先,这是一个CLR演变过程中极为有用的知道原则,例如,简单的东西必然时容易的,因此,在运行时增加可见的复杂度时不应该的。更重要的时,比起消费比,暴露复杂度带来了更大的代价,理想情况下,这个比例是负的(即,简化编程通过消除限制或归纳现有的特殊场景降低了复杂性,),但是,更典型的通过尽量减少暴露的复杂性和尽量多的使用这条准则降低了比率。

简化编程的第二个重要原因时医用性时CLR成功的根本原因。在更小和更快的执行文件方面,CLR没有本地代码好(通常编写好的本机代码会获胜),而CLR由于支持各种特性,功能(如垃圾回收、平台独立性、面向对象编程和版本支持),CLR在这方面不能打败本地代码。CLR的成功之处在于其众多功能结合起来显著降低编程难度,一些重要但经常被忽视的易用性特征包括:

1.简化语法(如C和Visual Basic #比C++简单明显) 2.更简单得类库类型(例如,我们只有一个字符串类型,它是不变的,这大大简化了任何使用字符串的API) 3.强命名(例如,要求API使用完整的单词和一致的命名约定) 4.工具链(例如,Visual Studio使构建CLR应用程序很简单,和IntelliSense会找到正确的类型和方法来创建应用程序很容易)。

正是这种易于使用的原则(这与用户模型的简单性结合在一起)突出了CLR成功的原因。奇怪的是,一些最重要的易用性特征也是最“枯燥”的,例如,任何编程环境都可以使用一致的命名约定,但实际上在一个大的类库中这样做是相当多的工作。通常这些努力与其他目标(如保留现有的接口兼容)会发生冲突,或者他们陷入严重的取舍问题(如重命名在非常大型代码库方法的成本)。正是在这样的时刻,我们必须提醒自己我们的首要目标,并确保我们优先达到这个目标。

#clr 的基本功能 运行时有很多特性,概括分类如下: 1.  基础特性-这些特性对其他的特性设计产生了广泛的影响   a. 垃圾回收   b. 内存安全,类型安全   c. 高级语言特性

2.辅助功能–由许多有用的程序可能不需要的基本功能启用的功能:   a. 应用程序域隔离   b. 程序安全/沙箱模式

3.其他功能-所有运行时环境都需要但不利用 clr 的基本功能的功能。 相反, 它们是创建完整编程环境的需求的结果。 其中包括:   a. 版本   b. 调试/分析   c. 互操作

##垃圾回收

在clr提供的所有特性中,垃圾回收机是最引人注目的。GC是自动内存回收的常用术语,在一个垃圾回收系统中,开发者不用在编程过程中操作特定的操作符去释放内存,运行时会自动保存垃圾回收堆中获取所有的内容引用,运行时会不时地遍历所有的内存引用,找到垃圾回收堆中哪些内存对象的引用仍旧被使用的对象,所有其他的对象就是垃圾,可以被回收并释放内存空间。

由于垃圾回收机大大简化了编程,他被认为是个十分有用的特性,最显而易见的是,手动执行删除指令释放内存不再必要,减少了开发人员产生错误的几率。

1.垃圾回收简化了接口设计,因为你不再需要小心的制定哪些接口负责删除通过接口的对象。举例而言,CLR接口简化了字符串返回,不在采用字符串缓存和长度,这意味着不在需要处理当buffer长度太小产生的错误,进而避免了由此带来的复杂度。因此,CLR垃圾回收机运行时接口比其他的更简单。

2. 开发者非常容易弄错特定对象的生命周期,是删除的太早了呢(导致内存崩溃),还是删除的太晚,导致内存泄漏。由于一个经典的程序包含数百万个对象,这产生错误的几率十分高,除此之外,追钟bug的生命周期是十分困难的,特别是当对象又被许多其他的对象引用,查找这类错误令人崩溃。而垃圾回收机消除了整个类型的常见错误,

  尽管如此,这也不是在这里特别提及垃圾回收的最重要的原因,更重要的是它是CLR运行时的基本要求。   >垃圾回收机要求GC堆上所有的对象引用都是可以追踪的。

  虽然这是个简单的要求,事实上,它对运行时有深刻的影响,如你所想,在程序运行的任意时刻,获取对象的所有引用是相当有难度的。我们有一个缓解的思路,技术上来说,这个简单的要求只是在垃圾回收发生时达成即可(从理论上来说 我们并不需要时时刻刻知道所有GC对象的引用,只要在GC发生的时候知道)。然而,实际情况下,这个方法由于CLR其他的特性并不是完全适用。

  >CLR 支持一个进程中同时运行多条线程

  任意时刻,其他的线程可能执行一个会引发垃圾回收的内存分配,在所有正在执行的线程中,指令的确切次序时无法预估的,我们不能说清楚知道哪一个线程内存分配请求会导致一个GC,另一条线程在做什么。因此,GC可能在任意时刻出现,CLR并不要求对线程的GC请求立即做出反应,因此CLR有一些回旋的空间,并不需要在时时刻刻追踪GC堆上的所有对象引用。但是它需要在足够的空间这样做,以保证“及时”的响应另一个线程上的分配导致的GC。

  这意味着多数时间CLR需要追踪GC堆上的所有引用,由于GC上的引用可能存储在机器寄存器,局部变量,静态字段或者其他字段,所以有相当多的位置需要追踪,所有的位置中最不能确定的是机器寄存器和局部变量,因为这和用户运行的code紧密相关,GC引用的机器代码还有其他的要求,必须追踪它使用的所有GC引用,这意味着编译器为了追踪对象需要做更多的工作。

  要了解更多内容, 请查看 [Garbage Collector design document](garbage-collection.md).

##托管代码概念 代码做额外的标记以便于它能报告实时的GC引用,这种代码成为托管代码,反之则是非托管代码。因此,在CLR之前的代码都是非托管代码(ava应该也是托管代码),实际上,所有的操作系统时非托管的代码。

###栈展开问题

由于托管代码依赖于操作系统提供的某些功能,所以有时会发生托管代码调用了非托管的代码,类似的,由于操作系统最初启动了托管代码,也会发生非托管代码调用了托管代码。因此,在托管代码运行的某一时刻,调用栈可能是一个包含了托管代码和非托管代码创建的混合帧。

非托管在运行时对栈帧并没有什么要求。特别是当没有要求他们在展开栈帧时寻找他们的调用者。因此,如果你在某一时刻停止了程序,而此时恰巧运行在一个非托管的方法中,通常情况下无法找到这个方法的调用者。而在degugger模式下可以找到,因为PDB文件种包含一些额外的信息。但是这些信息并不能确保可以获取调用者(这就是有时调试没能获得正确堆栈追踪的原因),然而对于托管代码很成问题,因为包含托管代码帧(保存了GC堆上的引用)的栈无法展开。

托管代码还有额外的要求:不仅要追踪当前栈执行过程中的所有GC对象引用,也要能展开他的调用者。此外,无论何时托管代码和非托管代码发生了互相调用,运行时都要额外标记所有的无法展开的非托管代码。,托管代码将所有托管栈帧连接起来,代码栈帧,至少可以始终可以找到与托管代码对应的堆栈块, 并枚举这些块中的托管帧。

[1] 更加最近的平台 m ABIs (application binary interfaces) 定义惯例为编码这个信息, 然而通常没有严格要求遵循。

### 托管代码

CLR在托管代码和非托管代码之间转换的时候都会有特别记录,托管代码以来这个记录来有效的掌握整个执行过程,托管的世界和非托管的世界存在极大的区别。此外,因为托管代码的运行遵循一个特定的CLR标准(MSIL),CLR将它转换符合本地环境的native coe。CLR对执行的内容有了更多的控制,来个栗子,CLR能够改变字段获取和调用函数的语义,事实上,CLR通过这种方式实现MarshalByReference对象的创建。看上去是个普通的本地对象,但是事实上确实存在于另一台机器上。简而言之,CLR有大量的"执行钩子"能够支持下面提到的种种强大特性。

另外:托管代码还有一个重要但是却不明显的特性,在非托管代码种,不允许使用GC指针(因为非托管代码创建的对象无法追踪),并且在托管代码调用非托管代码时会产生一个记录,并带来额外的开销,就是说,托管代码种调用一个非托管函数时,通常不是一个好主意。非托管方法不会使用托管参数,也不会返回托管类型,这意味着托管代码种创建的对象和对象句柄都必须显示的释放,不幸的时,非托管的API不能采用CLR的功能,例如异常和继承,与托管代码的接口相比,非托管代码的接口往往是不太美妙的体验。

暴露给非托管代码开发这的非托管接口常常是经过封装的,来个栗子,当访问文件时。托管开发者并不需要直接调用操作系统提供的Win32 CreateFile接口,托管代码中 System.IO.File封装了这个功能。事实上,直接将非托管功能暴露出来是十分罕见的。

对非托管代码的封装可能在某些方面看上去并不好(更多的代码,却没太多的功能),事实上,这是有益的。记住,不要直接暴露非托管接口,而是去封装。原因是,运行时的首要目的是让编程更加容易,然而经典的非托管方法不够简单。通常情况下,非托管代码的接口设计不是处于易于使用的考虑,而是为了接口完整性。每个看到创建文件和创建进程方法参数的人都会不会觉得这是个简单的方法。幸运的是,这个功能在托管的世界得到全新的改变,尽管这改变常常看上去非常low(重新命名,简化,组织这写非托管功能),但是十分有用,CLR中十分重要的一份文档是 [Framework Design Guidelines][fx-design-guidelines],这份800多页的文档详细的描述了托管类库的组织方式。

由此,我们常常看到的托管代码在两方面区别于非托管代码 1.托管代码是一个不同世界。CLR惊喜的控制程序执行的方方面面(可能除了个别的指令),CLR会检测代码执行进入或者离开托管代码,这提供了多种有用的功能 2.调用非托管代码会产生额外的成本,同时非托管代码也不能使用GC,这事实上鼓励对非托管代码进行封装,可以简化这些接口并符合一组统一的命名和设计准则,从而产生一个非托管世界中没有的的一致性和可见性。 这两种特性对于托管代码的成功都非常重要。 垃圾收集器提供的一个不太明显但相当有用的功能就是内存安全。保证内存安全非常简单: 如果程序只访问已分配的内存 (但未被释放), 则它是内存安全的。这要求开发这没有指向一个随机地址(更精确的说是一个已经被过早释放的地址)的野指针,很明显,内存安全是每个程序都想要的,空指针总是会导致bug,查找空指针通常是很费劲的。

>GC 需要提供内存安全保证 垃圾回收避免开发者过早释放内存(这会导致访问未正确分配的内存),由此确保了内存安全。如果要确保内存安全(这使得程序员无法创建内存不安全的程序),必须要有一个GC,原因是复杂系统需要动态分配内存,其中对象的生命周期处于程序时刻的控制之下(与堆栈分配的或静态分配的内存不同, 后者具有高度约束的分配协议).再这样一个不受控的环境中,通过静态代码来判断显式删除语句是否正确几乎是不可能,实际上, 确定删除是否正确的唯一方法是在运行时检查它。 这正是 gc 所做的事情 (检查内存是否仍然活着)。 因此, 对于任何需要堆式内存分配的程序, 如果要保证内存安全, 那么你需要一个GC.

GC是确保内存安全所必须的,但并非充分的。GC并不会检查数组越界访问,也不会阻止尾字段访问(如果使用基和偏移计算计算该字段的地址),如果我们确实阻止了这些情况, 那么我们确实可以使程序员无法创建内存不安全的程序。

虽然 [公共中间语言] [cil 规范] (cil) 有可以获取和设置任意内存 (导致不可用内存安全) 的运算符, 但它也有以下内存安全运算符, 并且 clr 强烈鼓励在大多数编程中使用它们: 1.字段访问指令集(LDFLD, STFLD, LDFLDA),根据名字读写字段地址。 2.数组访问指令集(LDELEM, STELEM, LDELEMA),根据索引读写一个数组元素地址。所有数组都带有指示其长度的标签,它用来在每次存取时做越界检查。

通过在用户代码中使用这些指令集,而不是底层(且不安全的)内存读写 指令集,还可以规避其他不安全 [CIL][cil-spec] 的操作(如那些允许跳转到任意且可能是非法的地址),这些都是构建一个内存安全系统所必须的。但CLR不只做这个,它支持更严谨的规则:类型安全。

类型安全是指每次内存分配都跟一个类型关联。所有操作内存的指令从理念上都与类型关联。类型安全要求读写指定内存只能使用与其关联的类型有效的指令集。这不仅保障了内存安全(没有野指针),也对每个类型加了一层额外的保护。

这些类型相关的保障中有一个重要的性质就是类型的可见性要求(特别是对于字段来说)也被强制保证了。因此,如果一个字段被声明为私有(即只能被类型本身定义的函数可见),那么这个私密性要求会被所有类型安全的代码所遵守。比如说,某个类可能定义了一个名为count的字段来记录其名为table的集合里的元素个数。假设table和count字段都是私有的,而且只有更新这两个字段的代码同时更新两个,那么table集合里元素的个数和count字段的值同步这一点有了强有力的保证。无论是否了解类型安全,程序员都是使用类型安全的概念来推理程序逻辑的。CLR将类型安全从编程语言/编译器之间的简单约定,上升到可以在运行时遵守的规范了

###可验证代码 - 强制内存和类型安全

为了保证类型安全,程序执行的每个指令都需要检查其是否符合内存关联的类型要求。虽然可以在运行时做这个检查,但性能会非常慢。所以CLR采用 [CIL][cil-spec] 验证的概念,即根据[CIL][cil-spec] 静态分析程序来确认大部分指令集是类型安全的。运行时只用来补充静态分析不能检查的地方。实际上,运行时的检查次数很少。它们包括下面这些指令: 1.将一个基类的指针强制转换为派生类型(反过来的转换可以放在静态分析里)。 2.数组越界检查(如同内存安全一样的道理)。 3.将指针数组里的元素替换成一个新(指针)值。这点是因为CLR数组的自由转换规则(在后文分析)。

这些检查对CLR提了如下这些要求: 1.GC里所有的内存对象都要关联类型(这样强制转换操作才能实现)。类型信息必须对运行时可见,而且要丰富到可以判断强制转换是否有效(例如运行时需要知道类型的继承层次)。实际上,每个对象在GC堆的第一个字段就指向关联类型在运行时的数据结构对象。 2.所有的数组都必须包含其大小(用来做越界检查)。 3.数组必须知道其元素的完整类型信息。

幸运的是,有些开销很大的要求(给堆上的内存打标签)也是支持垃圾回收所必要的(GC需要知道正在扫描的对象所有字段信息),因此支持类型安全的额外成本实际上不高。因此,按照[CIL][cil-spec]验证代码加上少量的运行时检查,CLR可以保证类型安全(和内存安全)。尽管如此,在编程弹性上,额外的安全带来了很大的代价。CLR有直接的内存读写指令,为了保证代码可验证性,这些指令的使用范围很有限。如所有的指针运行都会使代码无法通过验证,因此很多C和C++的典型用法都不能在要通过验证的代码里使用;你必须使用数组。虽然这样让编码有点不舒服,但也不是很差(数组也很有用),而且好处是现成的(更少的“诡异”的bug)。

CLR强烈建议使用可验证的,类型安全的代码。即使这样,有时还是要用到无法验证的代码(主要是跟非托管代码交互)。CLR运行这样,但是最佳实践是尽量限制(类型)不安全的代码的使用。一般的程序只有极少部分的不安全代码,而其它的是类型安全代码。

##高级语言特性

支持垃圾回收对CLR产生了深远的影响,因为它需要所有的代码支持额外的记录,类型安全的需求也产生了深远的影响。类型安全要求程序细粒度的描述程序,字段,方法都要有详细的类型信息,这也强CIL支持类型安全的其他高级编程构造,实现这些高级特性也需要CLR支持,这些高级语言特性中最重要的是两个用于支持面向对象编程的两个基本元素: 继承和虚拟调用调度。

(in a mechanical sense) 继承是相当简单的,基本概念就是 继承类型的字段是基类型字段的超集,派生类字段包含基类字段,任意代码中,一个指向基类实例的指针都能都能通过一个派生示例的指针转换得到,代码依旧能正常工作。如果一个一个类派生值一个基类,那么基类能用的地方,派生类也可以使用。相同的代码能够在不同的类型中使用,这边是多态。为了让运行时需要知道类型的强制转换是可行的, 运行时必须知道类型的继承方式, 以便它可以验证类型安全。

虚方法是继承概念的推广产生的,它允许继承方法重写基类中的方法,虚方法能够在基类型变量上调用一个虚方法时时, 将根据运行时对象的实际类型调度到正确的重写方法,虽然这样的运行时调度逻辑能够在在运行库中没有直接支持的情况下使用基本 [cil] [cil 规范] 指令实现, 但它有两个缺陷。 1.它不会是类型安全 (在调度表中的错误是灾难性的错误) 2.不同面向对象语言都可能实现一种略微不同的实现其虚拟调度逻辑的方式。 因此, 语言之间的互操作性会受到影响 (一种语言不能从以另一种语言实现的基类型继承)。

因此,CLR直接支持基础的面向对象特征,可能的话,clr 尝试使其继承模型 "语言中立", 因为不同的语言可能仍然共享同一继承层次结构。不幸的时,这并非总是可行的,特别时多继承有不同的实现方式。CLR不之多继承多个带有字段的类型,但是支持从一些没有字段的特殊的类型(接口)上多继承。

重要的是要记住, 虽然运行库支持这些面向对象的概念, 但它不需要使用它们。 没有继承概念的语言 (例如, 函数式语言) 根本不使用这些工具。

###值类型(装箱)

面向对象编程的一个深刻而微妙的方面是对象标识的概念: 所有对象(由独立的分配)能够互相区别,尽管所有的字段都是相同的。 对象标识是通过引用 (指针) 而不是按值访问对象。如果两个变量指向相同的对象(指针指向相同的第内),改变其中一个变量就会印象另外一个变量。虽然有可能有一个纯面向对象的系统, 其中一切 (包括整数) 是一个对象 (Smalltalk-80 做到了),不幸的是, 对象标识的概念不是所有类型的良好(语义匹配)。  特别是, 程序员通常不把整数看作对象。 如果在两个不同的位置分配了 "1", 程序员通常会认为这两个“1”是相等的, 当然不希望对其中一个更新影响其他的实例。 实际上, 一种称为 "函数式编程语言" 的广泛的程序设计语言, 完全避免了对象标识和引用语义。(a certain amount of implementation "gymnastics" is necessary to undo this uniformity to get an efficient implementation  不知道怎么翻译?)  其他语言 (perl、java、javascript) 采用实用的方法, 并按值对某些类型 (如整数) 进行处理, 另一些则通过引用。 CLR也使用了这样的混合模型, 但与其他模式不同的是允许用户定义的值类型。

值类型的关键特性是: 1.每个值类型的局部变量、字段或数组元素都有不同数据副本。 2.当一个变量,字段,或者数组元素赋值给另一个时,值会拷贝过去 3.相等性始终只在变量中的数据 (而不是其位置) 定义。 4.每个值类型也有一个对应的引用类型, 其中只有一个隐式的未命名字段。 这称为它的装箱值。 装箱的值类型可以参与继承并具有对象标识 (尽管强烈劝阻使用装箱值类型的对象标识)。

值类型模仿c的构造体类型, 与 c 类似, 可以有指向值类型的指针, 但指针与结构类型是不同的类型。

###异常

另一个CLR支持的高级语法功能是异常,异常让开发者能在程序发生异常是抛出任意对象,当一个对象抛出是,运行时搜索调用栈,寻找一个方法能catch到这个异常,如果这个catch找到了,程序从这个位置继续执行,异常的作用在于避免了调用方法时没有充分检查导致的常见错误。异常避免了这类错误(因此,编程更加简单),CLR不出意外的支持了它。

一方面,异常避免了一类常见错误(没有检查函数返回),它并不能避免其他的错误(在发生故障时将数据结构恢复到一致状态)。当一个错误被catch到,通常情况下不能确定继续执行是否会导致其他的错误(由第一个错误导致),这是未来CLR可能未添加的功能,然而, 即使在当前实施的情况下, 异常也是向前迈出的一大步 (我们需要更进一步)。

### 参数类型化(泛型)

在CLR2.0之前,唯一的参数化类型是数组,所有其他的容器(例如 hashtable,lists,queues,等),所有的操作都是基于通用类型,创建list和dictionary会带来一个负面影响,值类型会被装箱添加到集合中,在获取的时候拆箱。不过, 这并不是将参数化类型添加到 clr 的首要原因。 主要原因是参数化的类型使编程更容易;

原因十分未免,想象一下所有的类库中的类型都被通用类型取代, 这种效果与动态类型语言 (如 javascript) 中的情况不同。在CLR中,开发人员有更多出错的可能,该方法的参数是一个列表,字符串,数值,还是别的什么。无法从方法的签名上面明确的区分,更糟的是,当一个方法返回一个对象,还能作为哪些方法的对象?典型框架有数百个方法,如果他们的参数都是Object类型,那么很难确定哪些对象实例可以作为方法的参数,简而言之,强类型系统帮助开发者更清晰的表达意图,也能够让工具(eg,编译器)按照他的想法运作。这都能大大提高生产率。

这些好处不会因为类型被放入列表或字典中而消失, 因此, 参数化类型是有用的。 唯一真正的问题是, 参数化类型是由 cil 生成时 "已编译" 的语言特定功能, 还是应在运行时具有第一个类支持。 任何一种实现都是可能的。 clr 团队选择了后者, 因为没有它, 参数化类型在不同的语言中会有不同的实现方式。 如果这样,那么互操作性充其量只能是累赘。 此外, 参数化类型最类库接口设计上能更好的帮助开发者表达自己的意图。 如果 clr 未正式支持参数化类型, 则类库就无法使用它们, 而且一个重要的可用性功能将会丢失。

###程序数据  反射

CLR的基本原理是垃圾回收机,类型安全,和一些高级语言特性,这些特性是的程序规范也变得相当(复杂?)。如果这些丰富的运行时数据公开给开发这会是十分有价值的,这个想法催生了S有stem.Relection接口(允许程序反射程序信息)。这个接口几乎暴露了程序的方方面面,包括类型,继承关系,方法字段等等,事实上几乎没有信息丢失也使得翻遍你托管程序成为可能,而那些主演与知识产权保护有关的人对CLR的反射能力(能够通过代码混淆来有目的的破坏这些信息)惊呆了,托管代码中确实可以获取到丰富的信息。

除了在运行时简单地检查程序, 还可以对它们执行操作 (例如, 调用方法、设置字段等), 还可以更加强大,在运行时从头开始生成代码 (System.Reflection.Emit)。 实际上, 运行库使用此功能为匹配字符串 (System.Text.RegularExpressions) 创建专用代码, 并为序列化对象而生成代码以存储在文件或通过网络发送。 这样的功能在以前是不可行的 (您必须编写一个编译器!), 但是多亏了运行时, 在许多编程问题的范围之内。

虽然反射能力确实很强大, 但这种力量应该谨慎使用。 反射通常比静态编译的对应项慢得多。 更重要的是, 自我反射系统天生就难以理解。 因此这强大的功能, 如反射或反射. 只应在需求十分清晰的时候使用。

最后一组运行时功能与 clr 的基本体系结构 (gc、类型安全、高级规范) 无关, 但仍然填补了完整运行系统的重要需求。 # Other Features

##非托管代码互操作

托管代码能够使用在非托管代码中实现的功能。 互操作有两种主要的方式。 首先是简单地调用非托管函数 (这称为平台调用或 pinvoke) 的能力。 非托管代码还有一个称为 com (组件对象模型) 的互操作的 object-oriented 模型, 它的结构比方法调用结构化的多(??)。 由于 com 和 clr 都有对象和其他约定的模型 (如何处理错误、对象的生存期等), 因此, 如果 clr 具有特殊支持, 则可以更好地与 com 代码进行交互操作。

##预编译 clr 模型中, 托管代码作为 cil 而不是native code分发。运行时动态的转换为native code,有种优化方式,转化CIL得到的native code代码能使用一个叫做crossgen的工具保存在文件里面,这节省了大量的运行时编译时间,因为类库可能相当庞大.

##线程

CLR预期支持托管代码中的多线程程序。 从一开始, clr 库包含 System.Threading.Thread 类, 它是一个1到1的包装, 它是一个执行线程的操作系统概念。 但是, 由于它只是操作系统线程上的一个包装, 因此创建 System.Threading.Thread 相对昂贵 (启动时需要几毫秒)。 虽然这对于许多操作来说都很好, 但一种应用场景会创建非常小的工作项 (只占用数十毫秒)。 这在服务器代码中非常常见 (例如, 每个任务仅服务于一个 web 页), 或者在试图使用多进程(例如, 多核排序算法)。 为了支持这一点, clr 有一个程池的概念, 它允许工作排队。 在此方案中, clr 负责创建必要的线程来执行该工作。 当 clr 将程池直接公开为 System.Threading.Threadpool 类时, 首选的机制是使用 [任务并行库] (https:///或 msdn. 微软. com/美国/图书馆/dd460717 (v = 110). aspx), 它为非常常见的并发控制形式添加了额外的支持。

从实现的角度来看, 程池的重要创新在于它确保使用最佳的线程数来分派工作。 clr 使用反馈系统进行此项处理, 运行过程中监视吞吐量率和线程数, 并调整线程数以最大化吞吐量。 现在程序员可以考虑的主要是 "公开并行性" (即创建工作项), 而不是考虑更底层的问题, 即确定适当的并行量 (取决于工作负载和运行程序的硬件)。

# 总结 哦! 运行时做了很多!它用了许多页来描述运行时的一些功能, 但是没有开始讨论内部细节。 然而, 希望这一导言将为更深入了解这些内部细节提供一个的框架。 这个框架的基本轮廓是:

-运行时是支持编程语言的完整框架 -运行时的目标是让编程更加容易 -运行时的基础特征   - 内存回收   - 类型安全   - 高级语言特征

## 额外参考

- [MSDN Entry for the CLR][clr] - [Wikipedia Entry for the CLR](http://en.wikipedia.org/wiki/Common_Language_Runtime) - [ECMA Standard for the Common Language Infrastructure (CLI)][ecma-spec] - [.NET Framework Design Guidelines](http://msdn.microsoft.com/en-us/library/ms229042.aspx) - [CoreCLR Repo Documentation](README.md)

[clr]: http://msdn.microsoft.com/library/8bs2ecf4.aspx [ecma-spec]: ../project-docs/dotnet-standards.md [cil-spec]: http://download.microsoft.com/download/7/3/3/733AD403-90B2-4064-A81E-01035A7FE13C/MS%20Partition%20III.pdf [fx-design-guidelines]: http://msdn.microsoft.com/en-us/library/ms229042.aspx

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏24K纯开源

Python 3.x自定义迭代器对象

Python 3.x与Python 2.x之间存在着较多的语法细节差异。今天在看Python核心编程的时候,说到了自定义迭代器对象。于是动手将源码打了一遍,原书...

2236
来自专栏我的技术专栏

Java多线程编程—锁优化

1517
来自专栏FreeBuf

逆向工厂(二):静态分析技术

* 本文原创作者:追影人,本文属FreeBuf原创奖励计划,未经许可禁止转载 前言 [逆向工厂]第一章节中介绍了逆向技术的相关基础知识,其中提到逆向的两种形式:...

4488
来自专栏小灰灰

动手实现MVC: 3. AOP实现准备篇动态代理

背景 在实现AOP功能时,必然扰不开代理模式,所以在此之前,先准备下代理模式相关知识点 代理 关于代理,主要需要注意以下几个点 什么是代理模式 为什么要用代理 ...

1777
来自专栏有趣的Python

4-C++远征之起航篇-学习笔记

链接: https://pan.baidu.com/s/1SgdThGYaLDyXDFKvaBSa5A 密码: 2333

1294
来自专栏决胜机器学习

设计模式专题(八) ——模板方法模式

设计模式专题(八) ——模板方法模式 (原创内容,转载请注明来源,谢谢) 一、概念 1)含义 模板方法模式是为了让重复的内容都在父类实现,而避免重复。当完成某...

3546
来自专栏Android机动车

设计模式——代理模式

现在有个非常流行的程序叫做面向切面编程(AOP),其核心就是采用了动态代理的方式。怎么用?Java为我们提供了一个便捷的动态代理接口 InvocationHan...

1021
来自专栏奇点大数据

用python做科学计算之pandas入门简介

pandas是一个开源的python数据分析和处理包,使用灵活方便,性能高,速度快,简单介绍一下它里面比较常用的功能 数据读取 它支持多种数据读取的方式这里简...

2916
来自专栏决胜机器学习

设计模式专题(五)——工厂方法模式

设计模式专题(五)——工厂方法模式 (原创内容,转载请注明来源,谢谢) 一、概述 1、工厂方法与简单工厂模式区别 工厂方法模式与简单工厂模式不同 简单工厂模...

3869
来自专栏海说

深入理解计算机系统(3.2)---数据格式、访问信息以及操作数指示符

  本文的内容其实可以成为汇编语言的基础,因为汇编语言大部分时候是在操作一些我们平时开发看不到的东西,因此本文的目的就是搞清楚,汇编语言都是在操作些什么东西。或...

1304

扫码关注云+社区

领取腾讯云代金券