-
一、简介
程序员可能经常在.NET平台上开发应用程序,但却不一定都了解使.NET成为现实的3个关键(而且相互关联的)实体:CLR、CTS和CLS。从程序员的角度看,.NET可以理解为一个运行时环境和一个全面的基础类库。如图1所示,可以从宏观上看到CLR、CTS、CLS和基础类库之间的关系。
图1 CLR、CTS、CLS和基础类库之间的关系
运行时环境的正式名称是CLR(Common Language Runtime,公共语言运行时)。其主要作用是为我们定位、加载和管理.NET类型,同时也负责一些低层细节的工作,如内存管理、应用托管、处理线程、安全检查等。在运行时,CLR根本不关心开发人员用哪一种语言写源代码。这意味着在选择编程语言时,应选择最容易表达自己意图的语言。可用任何编程语言开发代码,只要编译器是面向CLR的。
.NET平台的另一个构造块是CTS(Common System,通用类型系统)。CTS规范完整描述了运行库所支持的所有可能的数据类型和编程结构,指定了这些实体间如何交互,也规定了它们在.NET元数据格式中的表示。
但是,一种特定的支持.NET的语言可能不支持CTS所定义的所有特性,因此出现了CLS(Common Language Specification,公共语言规范)。CLS是一个相关的规范,定义了一个让所有.NET语言都支持的公共类型和编程结构的子集。这样,如果构造的.NET类型仅公开与CLS兼容的特性,那么可以肯定其他所有支持.NET的语言都能使用它们。反之,如果使用了与CLS不兼容的数据类型或编程结构,就不能保证所有的.NET语言能和你的.NET代码库交互。
-
二、基础类库的作用
除了CLR和CTS/CLS规范之外,.NET平台提供了一个适用于全部.NET程序语言的基础类库(BCL)。这个基础类库不仅封装了各种基本类型,如线程、文件输入/输出(I/O)、图形绘制以及与各种外部硬件设备的交互,还支持在实际应用中用到的一些服务。
例如,基础类库定义了一些可创建任意类型软件应用程序的类型,例如,使用ASP.NET创建Web站点,使用WCF创建网络服务,使用WPF创建桌面GUI应用程序,等等。基础类库还定义了另外一些类型,可以与特定计算机上的XML文档、本地目录和文件系统互动,通过ADO.NET与关系数据库交流,等等。
-
三、CLR(公共语言运行时)
从编程角度来说,运行时环境(runtime)可以理解为执行给定编译代码单元所需的外部服务的集合。比如,当Java程序员向一台新电脑部署软件时,要确保软件运行,电脑上就要安装JVM(Java Virtual Machine,Java虚拟机)。
NET平台提供了另一种运行时环境。.NET运行库与刚才提到的其他运行库的关键不同在于,.NET运行时环境提供了一个定义明确的运行时层,可以被支持.NET的所有语言和平台所共享。
CLR中最重要的部分是由名为mscoree.dll的库(又称公共对象运行时执行引擎)物理表示的。当用户程序引用一个程序集,要使用它时,mscoree.dll将首先自动加载,然后由它负责将需要的程序集导入内存。运行时引擎负责许多任务,首要的任务是负责解析程序集的位置,并通过读取其中包含的元数据,在二进制文件中发现所请求的类型。接着,CLR在内存中为类型布局,将关联的CIL编译成特定平台的指令,执行所有需要的安全检查,然后运行当前的代码。
除了导入自定义的程序集和建立自定义的类型,必要时CLR也会与包含在.NET基础类库的类型交互。虽然完整的基础类库被分为若干分离的程序集,但最重要的程序集是mscorlib.dll。mscorlib.dll包含大量核心类型,它们封装了各种常见的编程任务与.NET语言用到的核心数据类型。当建立一个.NET解决方案时,你可以自动访问这些程序集。
图2说明了发生在源代码(它使用了许多基础类库类型)、.NET编译器和.NET执行引擎之间的工作流。
图2 mscoree.dll工作流
-
四、CTS(通用类型系统)
CLR一切都围绕类型展开,类型向应用程序和其他类型公开了功能。通过类型,用一种编程语言写的代码能与用另一种编程语言写的代码沟通。由于类型是CLR的根本,所以Microsoft制定了一个正式的规范来描述类型的定义和行为,这就是"通用类型系统"(Common Type System,CTS)。
一个给定的程序集可能包含任意数量的不同"类型"。在.NET领域,类型(type)是一个一般性的术语,它指的是集合{类,接口,结构,枚举,委托}里的任意一个成员。当用支持.NET的语言构建解决方案时,很有可能要与这些类型打交道。例如,程序集可能定义了一个类,它又实现了一些接口。或许其中某个接口方法采用枚举类型作为输入参数,而在调用时返回一个结构。
CTS(通用类型系统)是一个正式的规范。它规定了类型必须如何定义才能被CLR承载。通常,只有那些创建针对NET平台的工具或编译器的人才对CTS的内部工作非常关心。但是,对于所有.NET编程人员来说,学习如何在自己使用的语言中使用由CTS定义的5种类型,是非常重要的。
CTS规范规定,一个类型可以包含零个或者多个成员。这些成员可以为如下的类型:
-
字段(Field)
作为对象状态一部分的数据变量。字段根据名称和类型来区分。
-
方法(Method)
针对对象执行操作的函数,通常会改变对象状态。方法有一个名称、一个签名以及一个或多个修饰符。签名指定参数数量(及其顺序);参数类型:方法是否有返回值;如果有返回值,还要指定返回值类型。
-
属性(Property)
对于调用者,属性看起来像是字段。但对于类型的实现者,属性看起来像是一个方法(或者两个方法。属性允许在访问值之前校验输入参数和对象状态,以及/或者仅在必要时才计算某个值。属性还允许类型的用户采用简化的语法。最后,属性允许创建只读或只写的"字段"
-
事件(Event)
事件在对象以及其他相关对象之间实现了通知机制。例如,利用按钮提供的一个事件,可在按钮被单击之后通知其他对象。
CTS还指定了类型可见性规则以及类型成员的访问规则。例如,如果将类型标记为public(在C#中使用public修饰符),任何程序集都能看见并访问该类型。但是,如果标记为assembly(在C#中使用internal修饰符),只有同一个程序集中的代码才能看见并访问该类型。所以,利用CTS制定的规则,程序集为一个类型建立了可视边界,CLR则强制(贯)了这些规则。
调用者虽然能"看见"一个类型,但并不是说就能随心所欲地访问它的成员。可利用以下选项进一步限制调用者对类型中的成员的访问。
-
private
成员只能由同一个类(class)类型中的其他成员访问。
-
family
成员可由派生类型访问,不管那些类型是否在同一个程序集中。注意,许多语言(比如C++和C#)都用protected修饰符来标识family。
-
family and assembly
成员可由派生类型访问,但这些派生类型必须在同一个程序集中定义。许多语言(比如C#和Visual Basic)都没有提供这种访问控制。当然,IL汇编语言不在此列。
-
assembly
成员可由同一个程序集中的任何代码访问。许多语言都用internal修饰符来标识assembly。
-
family or assembly
成员可由任何程序集中的派生类型访问。成员也可由同一个程序集中的任何类型访问。C#用protected internal修饰符标识family or assembly。
-
public
成员可由任何程序集中的任何代码访问。
除此之外,CTS还为类型继承、虚方法、对象生存期等定义了相应的规则。例如CTS规定一个类型只能从一个基类派生(单继承),且所有类型最终必须从预定义的System.Object类型继承。
CTS需要关注的最后一个方面是,它建立的一套定义明确的核心数据类型。尽管不同的语言通常都有自己唯一的用于声明内建CTS数据类型的关键字,但是所有语言的关键字最终将解析成定义在mscorlib.dll程序集中的相同类型。参考表1,它描述了如何在不同的.NET语言中表示关键的CTS数据类型。
CTS数据类型
VB关键字
C#关键字
C++/CLI关键字
System.Byte
Byte
byte
unsigned char
System.Sbyte
Sbyte
sbyte
signed char
System.Int16
Short
short
short
System.Int32
Integer
int
int 或 long
System.Int64
Long
long
__int64
System.UInt16
UShort
ushort
unsigned short
System.UInt32
Uinteger
uint
unsigned int 或 unsigned long
System.UInt64
ULong
ulong
unsigned __int64
System.Single
Single
float
float
System.Double
Double
double
double
System.Object
Object
object
object^
System.Char
Char
char
wchar_t
System.String
String
string
String^
System.Decimal
Decimal
decimal
Decimal
System.Boolean
Boolean
bool
bool
表1内建的CTS数据类型
-
-
五、CLS(公共语言规范)
不同语言创建的对象可通过COM相互通信。CLR则集成了所有语言,用一种语言创建的对象在另一种语言中,和用后者创建的对象具有相同地位、之所以能实现这样的集成,是因为CLR使用了标准类型集、元数据(自描述的类型信息)以及公共执行环境。
语言集成是一个宏伟的目标,最棘手的问题是各种编程语言存在极大区别。例如,有的语言不区分大小写,有的不支持unsigned(无符号)整数、操作符重载或者参数数量可变的方法。
要创建很容易从其他编程语言中访问的类型,只能从自己的语言中挑选其他所有语言都支持的功能。为了在这个方面提供帮助,Microsoft定义了"公共语言规范"(Common Language Specification,CLS),它详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS、面向CLR的语言生成的组件。
CLR/CTS支持的功能比CLS定义的多得多,CLS定义的只是一个子集。所以,如果不关心语言之间的互操作性,可以开发一套功能很全的类型,它们仅受你选择的那种语言的功能集的限制。具体地说,在开发类型和方法时,如果希望它们对外"可见",能从符合CLS的任何编程语言中访问,就必须遵守CLS定义的规则。注意,假如代码只是从定义(这些代码的)程序集的内部访问,CLS规则就不适用了。图3形象地演示了这一段想要表达的意思。
图3每种语言都提供了CLR/CTS的一个子集以及CLS的一个超集(但不一定是同一个超集)
如图3所示,CLR/CTS提供了一个功能集。有的语言公开了CLR/CTS的一个较大的子集。如果开发人员用IL汇编语言写程序,可以使用CLR/CTS提供的全部功能。但是,其他大多数语言(比如C#、Visual Basic和Fortran)只向开发人员公开了CLR/CTS的一个功能子集。CLS定义了所有语言都必须支持的最小功能集。
用一种语言定义类型时,如果希望在另一种语言中使用该类型,就不要在该类型的public和protected成员中使用位于CLS外部的任何功能。否则,其他开发人员使用其他语言写代码时,就可能无法访问这个类型的成员。
以下代码使用C#定义一个符合CLS的类型。然而,类型中含有几个不符合CLS的构造,造成C#编译器报错:
using System; // 告诉编译器检查CLS相容性 [assembly: CLSCompliant(true)] namespace SomeLibrary { // 因为是public类,所以会显示警告 public sealed class SomeLibraryType { // 警告:SomeLibrary.SomeLibraryType.Abc()的返回类型不符合CLS public UInt32 Abc() { return 0; } // 警告:仅大小写不同的标识符SomeLibrary.SomeLibraryType.abc()不符合CLS public void abc() { } // 不显示警告:该方法是私有的 private UInt32 ABC() { return 0; } } }
上述代码将[assembly:CLSCompliant(true)]这个特性应用于程序集,告诉编译器检查其中的任何公开类型,判断是否存在任何不合适的构造阻止了从其他编程语言中访问该类型。上述代码编译时,C#编译器会报告两条警告消息。第一个警告是因为Abc方法返回无符号整数,一些语言是不能操作无符号整数值的。第二个警告是因为该类型公开了两个public方法,这两个方法(Abc和abc)只是大小写和返回类型有别。Visual Basic和其他一些语言无法区分这两个方法。
有趣的是,删除sealed class SomeLibraryType之前的public字样,然后重新编译,两个警告都会消失。因为这样一来,SomeLibraryType类型将默认为internal(而不是public),将不再向程序集的外部公开。
现在提炼一下CLS的规则。在CLR中,类型的每个成员要么是字段(数据),要么是方法(行为)。这意味着每一种编程语言都必须能访问字段和调用方法。字段和方法以特殊或通用的方式使用。为简化编程,语言往往提供了额外的抽象,从而对这些常见的编程模式进行简化。例如,语言会公开枚举、数组、属性、索引器、委托、事件、构造器、终结器、操作符重载、转换操作符等概念。编译器在源代码中遇到其中任何一样,都必须将其转换成字段和方法,使CLR和其他任何编程语言能够访问这些构造。