CTS、CLS、CLR
CTS
一个给定的程序集可能包含任意数量的不同“类型”。在.NET领域,类型(type)是一个一般性的术语,它指的是集合{类,接口,结构,枚举,委托}里的任意一个成员。当用支持.NET的语言构建解决方案时,很有可能要与这些类型打交道。例如,程序集可能定义了一个类,它又实现了一些接口。或许其中某个接口方法采用枚举类型作为输入参数,而在调用时返回一个结构。
CTS(公共类型系统)是一个正式的规范,它规定了类型必须如何定义才能被CLR承载。通常,只有那些创建针对.NET平台的工具或编译器的人才对CTS的内部工作非常关心。但是,对于所有.NET编程人员来说,学习如何在自己使用的语言中使用由CTS定义的5种类型,是非常重要的。这里简单概括一下。
CTS类类型
每一种支持.NET的语言至少要支持类类型(class type)的概念,这是OOP的基础。类可能由很多成员(诸如构造函数、属性、方法和事件)和数据点(字段)组成。在C#中,使用class关键字来声明类。
表1-1给出了有关类类型的一些特征。
表1-1 CTS类类型
类的特征 | 在生命周期里的意义 |
类是否被“密封” | 密封类不能作为其他类的基类 |
类实现任何接口了吗 | 接口是抽象成员的集合,它在对象和对象的用户间提供一个契约。CTS允许类实现任何数目的接口 |
类是具体的还是抽象的 | 抽象类是不能直接创建的,但是可以用来为派生类型定义公共的行为。具体类可以直接创建 |
这个类的可见性是什么 | 每个类必须用关键字(public或internal)设置可见性。基本上,可见性定义了该类是被外部程序集使用,还是仅能在定义了它的程序集中使用 |
CTS接口类型
接口(interface)就是由抽象成员定义所组成的一个具名集合,可通过一个给定的类或结构来实现。在C#中,接口类型使用interface关键字来定义。一般情况下,所有的.NET接口均以大写字母I开头。
就它们自身而言,接口没有什么用。然而,当一个类或结构用其独特方式来实现一个给定接口时,你将能够以多态方式通过接口引用来请求使用所提供的功能。
CTS结构类型
CTS中还支持结构(structure)的概念。简单地说,结构(struct)可以看做是具有值语义的轻量级类类型。通常,结构最适合建模几何和数学数据。在C#中,通常使用struct关键字创建结构。
CTS枚举类型
枚举(enumeration)是一种便利的编程结构,它可以用来组成名称/值对。例如,假设你在开发一个视频游戏的程序,要让玩家在3种角色(Wizard、Fighter或Thief)中选择一个。你完全可以用enum关键字来建立一个自定义的枚举,而不用老是要记着代表每种可能性的原始数字值。
在默认情况下,每一项是用一个32位的整数来存储的,但如果需要,也可以改变存储大小(例如,在为Windows移动设备之类的低内存设备编程时)。另外,CTS要求枚举类型派生自基类System.Enum。这个基类定义了一些有趣的成员,允许通过编程提取、操作和变换底层的名称/值对。
CTS委托类型
委托(delegate)在.NET中等效于类型安全的C风格的函数指针。它们的主要不同在于,.NET委托是派生自System.MulticastDelegate的类,而不是一个简单地指向原始内存地址的指针。在C#中,委托是使用关键字delegate来声明的。
一个实体可以用委托向另一个实体传递调用,另外,委托也为.NET事件架构提供了基础。委托对多路广播(即将一个请求转发给多个接收者)和异步方法调用(即从另一个线程调用方法)有着内在支持。
CTS类型成员
现在你已经看到了由CTS正式规定的各种类型,但还要认识到,大部分的类型可以含有任意数量的成员。说得更正式一些,类型成员是集合{构造函数,终结器,静态构造函数,嵌套类型,操作符,方法,属性,索引器,字段,只读字段,常量,事件}中的元素之一。
CTS定义了各种可能与具体成员关联的修饰语(adomment)。例如,每个成员都有一个给定的可见性特征(如公共的、私有的和受保护的等)。有些成员可能被声明成抽象的以加强派生类的多态性,有些成员可声明为虚拟的以定义一个封装(但可重写)的实现。同样,绝大部分成员可设置成静态的(在类级别绑定)或者实例(在对象级别绑定)。
內建的CTS数据类型
CTS需要关注的最后一个方面是,它建立的一套定义明确的核心数据类型。尽管不同的语言通常都有自己唯一的用于声明內建CTS数据类型的关键字,但是所有语言的关键字最终将解析成定义在mscorlib.dll程序集中的相同类型。参考表1-2,它描述了如何在不同的.NET语言中表示关键的CTS数据类型。
表1-2 內建的CTS数据类型
CTS数据类型 | VB关键字 | C#关键字 | C++/CLI关键字 |
System.Byte | Byte | byte | unsigned char |
System.SBtye | 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 |
由于各种委托语言的关键字只是System命名空间中真实类型的简化符号,我们不需要担心数值数据的上溢或下溢,或是字符串和布尔型数据在内部是怎样跨不同语言进行表示的。下面的代码片段使用C#和VB,通过语言关键字和正式的CTS数据类型分别定义了32位数值变量。
// 用C#定义整型数据 int i = 0; System.Int32 j = 0;
' 用VB定义整型数据 Dim i As Integer = 0 Dim j As System.Int32 = 0
CLS
不同的语言往往用不同的、语言特定的术语表达相同的程序构造,比如,在C#中使用加号(+)操作符表示字符串拼接,而在VB中却使用“&”符号。即使两种不同的语言表达相同的编程惯用法(比如一个不返回值的函数),在表面看起来,语法也可能非常不同。
在.NET运行库看来这些较小的语法变化是微不足道的,因而不同的编译器(这里用到的是vbc.exe或csc.exe)将产生类似的CIL指令集。然而,语言也可能在功能上不同,比如,.NET语言可能有也可能没有关键字来表示无符号数据,可能支持也可能不支持指针类型。对于这些可能的变化,理想情况是所有支持.NET的语言都有一个可以遵循的基准。
CLS就是这样一套规则,它清晰地描述了支持.NET的编译器必须支持的最小的和完全的特征集,以生成可由CLR承载的代码,同时可以被基于.NET平台的其他语言用统一的方式进行访问。CLS可以看成是由CTS定义的完整功能的一个子集。
如果打算让自己的产品功能无缝地融合到.NET世界,那么CLS是编译器创建者最终必须要遵循的一套规则。每个规则被赋予一个简单的名字(如CLS规则6),描述了这个规则如何影响创建编译器的人以及(以某种方式)与他们交互的人。影响最大的是规则1。
- 规则1:CLS规则仅适用于类型中向定义它的程序集以外公开的部分。
根据这个规则,可以(正确地)推断其余的CLS规则对于用来建立一个.NET类型内部运行功能的逻辑是不适用的。必须遵循CLS的类型的唯一一点,就是成员定义本身(即命名规范、参数和返回类型)。成员的实现逻辑可以使用其他的非CLS技术,程序外部并不知道这些不同。
举例说明,下面的Add()方法就没有遵循CLS规则,因为它的参数和返回值使用了无符号数(无符号数不符合CLS):
class Calc { //公开的无符号类型数据不遵循CLS规则 public ulong Add(ulong x, ulong y) { return x + y; } }
然而,如果像下面一样在程序内部使用无符号数:
class Calc { public int Add(int x, int y) { // 当ulong类型变量仅仅在内部使用时,仍然遵循CLS规则 ulong temp = 0; ... return x + y; } }
这仍然遵循CLS规则,可以保证所有的.NET语言都能调用Add()方法。
当然,除规则1外,CLS还定义了很多其他的规则。例如,CLS描述了一种语言如何表示文本字符串,如何在内部表示枚举(用于存储的基类型),如何定义静态成员,等等。好在你不需要记忆所有的规则也能成为静态.NET的程序员。总地来说,只有那些工具/编译器的开发人员才会对CTS和CLS规范的具体细节感兴趣。
确保遵循CLS
C#定义了一些不遵循CLS规则的程序结构,但你仍然可以使用一个专门的.NET特性指示C#编译器检查代码是否遵循CLS规则。
// 指示C#编译器检查是否遵循CLS规则 [assembly:CLSCompliant(true)]
目前,只需要知道[CLSCompliant]特性就是用来指示C#编译器按CLS规则检查每行代码的。如果代码违反了CLS,就会给出编译器错误和关于错误代码的描述。
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解决方案时,你可以自动访问这些程序集。
图1-4说明了发生在源代码(它是用来许多基础类库类型)、.NET编译器和.NET执行引擎之间的工作流。
图1-1 mscoree.dll工作流