不管选择了哪种.NET语言编程,需要明白的是,尽管.NET二进制文件与非托管Windows二进制文件(*.dll或*.exe)具有相同的文件扩展名,但它们的内部却是完全不同的。具体来说,.NET二进制文件不包含特定于平台的指令,他包含的是平台无关的IL(Intermediate Language,中间语言)和类型元数据。
图1 所有支持.NET的编译器都生成IL指令和元数据
当使用支持.NET的编译器生成*.dll或*.exe文件时,二进制大对象会被打包成一个程序集。
程序集包含CIL代码,后者在概念上类似于Java的字节码,因为它只在绝对必需的情况下才编译为特定平台的指令。“绝对必需”通常是指一段CIL指令(例如一个方法实现)被.NET运行库引用时。
除了CIL指令外,程序集还包含元数据(metadata)。元数据详尽描述了二进制文件中每个“类型”的特征。例如,一个名为SportsCar的类,这个类型的元数据描述了一些详细信息,比如SportsCar的基类,这个基类如果有接口,则其接口由SportsCar来实现,元数据同时也描述了由SportsCar类型支持的各种成员。.NET元数据总是存在并且会由某种支持.NET的编译器自动生成。
最后,处理CIL和类型元数据之外,程序集本身也使用元数据进行描述,这类元数据的正式名称是清单(manifest)。清单记录了程序集的当前版本信息、文化信息、(用于本地化字符串和图像资源)和正确执行所需的外部引用程序集的列表。
CIL的作用
现在我们来深入探讨CIL代码、类型元数据和程序集清单。CIL是一种和平台无关的语言。例如,下列的C#代码构成一个简单的计算器。现在不必在意具体的语法,只要注意Calc类中的Add()方法的格式:
C#编译器(csc.exe)编译这段代码后,就会得到一个单文件*.exe程序集,其中包含一个程序集清单、CIL指令和描述Calc与Program类的各方面信息的元数据。
例如,如果用ildasm.exe打开该程序集,会发现Add()方法被CIL表示为:
如果看不懂这段CIL代码,也不必担心。需要注意一点,C#编译器生成的是CIL,并不是平台相关的指令。
这一点适用于所有支持.NET的编译器。为了便于说明,我们假设用VB创建一个和上面C#相同的程序:
如果查看Add()方法的CIL指令,你会觉得它与VB编译器vbc.exe生成的代码非常相似(只有很少的差别):
1.CIL的好处
到此,你可能很想弄清楚,不直接把源代码编译为特定的指令集而是编译为CIL的好处到底在哪里。有一点好处就是语言的集成性。每种支持.NET的编译器生成的是几乎完全相同的CIL指令。因此,所有语言都能很好地在定义明确的二进制文件间交互。
此外,CIL是平台无关的,.NET Framework本身也是平台无关的。Java程序员早已体会到了这一点好处(例如,一个代码库就可以在多种操作系统上运行)。实际上,已经存在C#语言的国际标准和大量的.NET平台和实现的子集,它们可以提供许多非Windows的操作系统使用。
2.将CIL编译成特定平台的指令
由于程序集包含的是CIL指令而不是某一特定平台的指令,CIL代码必须在使用之前进行即时编译。将CIL代码编译成有意义的CPU指令的工具称为JIT(即时)编译器,有时也称为Jitter。.NET运行库环境将使用针对各种不同CPU的JIT编译器,每个编译器都会针对底层平台进行优化。
比如,在手持设备(如Windows移动设备)上部署一个.NET应用程序,就可以配备相应的Jitter以在低内存环境下运行。另一方面,如果为后台服务器部署程序集(通常内存不是问题),那么Jitter又能进行优化,使代码在高内存环境下运行。这样,开发人员只需要编写一套代码,就能在不同体系结构的设备上通过JIT编译器高效地编译和执行。
另外,当给定的Jitter编译器将CIL指令编译为相应的机器代码时,它会用适合目标操作系统的方式将结果缓存在内存中。这样,如果PrintDocument()方法被调用,则它对应的CIL指令将在第一次调用中被编译成特定平台的指令并保留在内存中以备以后使用。因此,在下一次调用PrintDocument()时,就不需要编译CIL了。
.NET类型元数据的作用
除了CIL指令以外,.NET程序集还包括全部完整且准确的元数据,这些元数据描述了每一个二进制文件中定义的类型(如类、结构、枚举等)以及每个类型的成员(比如属性、方法和事件等)。值得庆幸的是,生成最新的和最大的类型元数据总是编译器的工作而不是程序员的工作。因为.NET元数据非常详细,所以程序集完成了自描述的实体。
图2 元数据
元数据不仅用于.NET运行库环境的许多方面,而且用于各种开发工具中。例如,Visual Studio等工具提供的智能感知(IntelliSense)特性就能在设计阶段读取程序集的元数据。各种对象浏览工具、调试工具以及C#编译器自身都使用元数据。需要注意的是,元数据是许多.NET技术的支柱,这些技术包括WCF、反射、晚期绑定和对象序列化。
程序集清单的作用
最后,请记住.NET程序集也包含描述程序集自身的元数据(称为清单,manifest)。在许多细节中,清单记录了所有确保现有程序集正常工作的外部程序集、程序集的版本号、版权信息等。同类型元数据一样,生成程序集清单也是编译器的工作。下面是编译Calc.cs代码文件时所生成的清单的一些重要细节(假设我们指示编译器将程序集命名为Calc.exe):
图3 清单
简要地说,这个清单记录了Calc.exe(通过.assembly extern指令)所需要的外部程序集,同时也记录了程序集本身的各种特性(如版本号、模块名称等)。