上一篇:ECMA-335(CLI)标准 读书笔记(第一部:概念和架构 第7章)
8. Common Type System
类型描述了值并指定了该类型的所有值应该支持的契约(见8.6章)。因为CTS既支持面向对象编程(OOP)语言,也支持函数式和过程式编程语言,所以它涉及到两种实体:对象和值。值是简单的位模式,如整型和浮点型;每个值都有个类型来描述它所占用的存储空间和其呈现中位的意义,也能描述在呈现上所能做的操作。值用于代表诸如C编程语言中相应的简单类型,也要代表如C + +和Java™语言中非对象的东西。
对象比值可以做更多。每个对象都是自描述类型的,也就是说,它的类型是明确保存在其呈现里的。它有区别于其它所有对象的特性,并有位置存储其它的实体(这可能是对象或者值)。当内部位置上的内容被改变时,对象的特性不会变。
有几种对象和值,如下图资料显示。
注:托管指针可以指向堆中。
泛型特征允许用一种模式来定义一整套的类型和方法,其中包括叫做泛型参数的占位符。这些泛型参数需要通过特定类型被替换,实例化为该家族成员的实际需要。泛型的设计符合下列目标:
- 相关性:泛型类型可以出现在任何CLI类型存在的地方。
- 语言独立:不对源语言做出任何假设。但是,CLI的泛型试图支持尽可能多语言的现有类中的泛型功能。此外,设计允许对当前缺乏泛型的语言进行新扩展。
- 实现独立:一个CLI的实现被允许专门逐一实现呈现和代码,或可能通过装箱和拆箱值来共享所有的呈现和代码。
- 实现效率:泛型的性能并不比用对象仿真泛型的效率要差。一个好的实现要做得更好,就要避免在引用类型的实例化方面含糊不清,要对值类型的实例化产生专门的代码。
- 对定义做静态检查:泛型定义能被独立于实现来验证核实。这样,泛型类型被静态验证,它的方法对所有有效的实例化而言向JIT编译提供了保障。
- 考虑泛型参数的统一行为:总的来说,参数化的类型和泛型方法的行为在所有类型的实例化上是“相同的”。
另外,CLI支持协变和逆变泛型参数,带有下列特征:
- 类型安全(基于纯静态检查)
- 简单:特殊情况下的,不一致仅允许存在于泛型接口和泛型代理上(非类或值类型)
- 不希望支持差异的语言可以忽略这些特征,并将所有的泛型类型都作为一致的。
- 能够实现某些语言中更复杂的协作方案,如Eiffel。
8.1 面向对象编程的关系
术语类型(Type)经常被用于面向值编程的领域里来表示数据的呈现。在面向对象领域里经常指行为而不是呈现。在CTS中,类型被用于表达这两种意思:当且仅当两个实体有兼容的呈现和兼容的行为时,它们才有兼容的类型。这样,在CTS中,一个类型派生自一个基类型,那么派生类型的实例能被基类型的实例替代,因为两者的呈现和行为是兼容的。
与一些OOP语言不同,在CTS中,两个具有从根本上不同呈现的对象有不同的类型。一些OOP语言用了一个类型上的不同概念。它们认为如果两个对象以同样的方式响应同一套消息,那么它们就有相同的类型。这个概念在CTS中被这种说法取代了,即对象实现相同的接口。
同样的,一些OOP语言(如Smalltalk)认为消息传递是计算的基本模式。在CTS中,这相当于调用虚方法(见8.4.4章),这里虚方法的签名扮演着消息的角色。
CTS自身并不使用“无类型编程”的概念。也就是说,没有办法在不知道对象类型的情况下调用非静态的方法。尽管如此,无类型编程仍能在基于实现了反射的包(见第四部)提供的工具的基础上实现。
8.2 值和类型
类型描述值。任何被类型描述的值都是类型的一个实例。任何值的使用——存储它,作为参数传递它,操作它——都要求一个类型。这特别应用于所有的变量、参数、栈单元的赋值和方法结果上。类型定义了容许值和被类型的值支持的容许的操作。所有的操作符和函数应该具有每个可访问或使用的值的类型。
每个值都有一个确切的值来完整描述它的类型属性。
每个值是它的确切类型的一个实例,也能是其它类型的一个实例。特别是,如果一个值是一个派生自其它类型的一个类型的实例,那么它也是另外那个类型的实例。
8.2.1 值类型和引用类型
有两种类型:值类型和引用类型。
- 值类型——被一个值类型描述的值是自包含的(每个值都能在不引用其它值的情况下被理解)。
- 引用类型——被引用类型描述的值中记录了另一个值的位置。
有四种引用类型:
· 对象类型是一个自描述值的引用类型(见8.2.3章)。一些对象类型(如抽象类型)仅是一个值的部分描述。
· 接口类型总是一个值的部分描述,被许多对象类型潜在地支持。
· 指针类型是一个编译时的值描述,这些值的呈现是本地的一个机器地址。
· 内建的引用类型。
8.2.2 内建的值和引用类型
下面的数据类型是一个CTS的整体部分,被VES直接支持。它们在持久的metadata中有特殊的编码。
表1:特殊的编码
Table 1: Special Encoding
Name in CIL assembler (see Partition II) | CLS Type? | Name in class library (see Partition IV) | Description |
bool1 | Yes | System.Boolean | True/false value |
char1 | Yes | System.Char | Unicode 16-bit char. |
object | Yes | System.Object | Object or boxed value type |
string | Yes | System.String | Unicode string |
float32 | Yes | System.Single | JEC 60559:1989 32-bit float |
float64 | Yes | System.Double | JEC 60559:1989 64-bit float |
int8 | No | System.SByte | Signed8-bitinteger |
int16 | Yes | System.Int16 | Signed 16-bit integer |
int32 | Yes | System.Int32 | Signed32-bitinteger |
int64 | Yes | System.Int64 | Signed64-bitinteger |
native int | Yes | System.IntPtr | Signed integer, native size |
native unsigned int | No | System.UIntPtr | Unsigned integer, native size |
typedref | No | System.TypedReference | Pointer plus exact type |
unsigned int8 | Yes | System.Byte | Unsigned 8-bit integer |
unsigned int16 | No | System.UInt16 | Unsignedl6-bitinteger |
unsigned int32 | No | System.UInt32 | Unsigned32-bitinteger |
unsigned int64 | No | System.UInt64 | Unsigned64-bitinteger |
在上表中归类的bool和char 类型为整型。
8.2.3 类、接口和对象
一个类型如果明确得定义了一个值的呈现和定义在值上的操作,那么它就完整得描述了一个值。
对于一个值类型,定义呈现需要描述组成值呈现的位的次序。对一个引用类型而言,定义呈现需要描述组成值呈现的位的位置和次序。
方法描述了能在一个确切类型的值上所做的操作。定义一套在确切类型的值上的操作需要为每个操作命名方法。
一些类型仅是部分描述;例如接口类型。这些类型描述了一套操作的子集,没有呈现,因此不是任何一个值的确切呈现。因此,当一个值有一个确切类型的时候,它也可以是许多类型的一个值。进一步说,因为确切类型完整描述了一个值,它也完全指定了一个确切的值所能拥有的所有其它类型。
当每个值都有个确切类型的时候,不总是能通过查看值的呈现来决定它的确切类型。特别是,永远不可能决定一个值类型的值的确切类型。考虑两个内建的值类型,32位的有符号和无符号整数。当每个类型是它们各自值的完整说明时(如确切的类型),没有办法从一个值的专有的32位序列中得到确切的类型。
对于一些叫做对象的值,总是可能从值中获得确切的类型。对象的确切类型也叫做对象类型。对象是引用类型的值,但并不是所有引用类型描述对象。考虑一个指向32位整型的指针的值,一种引用类型。没有办法通过检查位信息来获取其值类型;因此它不是个对象。现在考虑内建的CTS引用类型System.String(见第四部分)。这个类型的值的确切类型总是通过检查值来确定,因此类型System.String的值是对象,System.String是对象类型。
8.2.4 值的装箱和拆箱
对每个值类型来说,CTS定义了一个统一的引用类型叫做装箱类型(boxed type)。反之是不成立的:一般而言,引用类型没有一个统一的值类型。一个装箱类型的值的呈现(装箱值)位于值类型的值被存储的地方。一个装箱类型是一个对象类型,装箱值是一个对象。
一个装箱类型不能通过名字被直接引用,因此没有字段或者本地变量能被给定一个装箱类型。最接近装箱枚举值类型的指定基类是System.Enum;对于所有其它的值类型是System.ValueType。类型为System.ValueType的字段仅能包含空值或者一个装箱值类型的实例。类型为System.Enum的本地变量仅能包含空值或者一个装箱枚举类型的实例。
所有的值类型有一个叫做装箱(box)的操作。装箱一个任意值类型的值产生它的装箱值;例如,一个统一的装箱类型的值包含了原值的按位拷贝。如果值类型是空类型——定义为值类型System.Nullable<T>的一个实例——结果是一个空引用或者类型T的值属性的按位拷贝,依赖于其HasValue属性(分别是false和true)。所有的装箱类型有个拆箱(unbox)操作,这就出现了一个指向值的位呈现的托管指针。
装箱指令不仅只能用于值类型;这样的类型被叫做可装箱(boxable)类型。如果类型是下面的一种情况那就是可装箱的:
- 一个不包含能指向CIL运算栈字段的值类型(包括泛型值类型的实例)。
【理由:一个值类型上有些字段不能被装箱,否则那些指向CIL运算栈顶的嵌入指针可能存在更长的时间。例如:System.RuntimeArgumentHandle, System.TypedReference。包含这样指针的值类型非正式的被描述为“按引用式(byref-like)”的值类型。】
- 一个引用类型(包括类、数组、代理和泛型类的实例化)
- 一个非托管的指针类型
- 一个泛型参数(对一个泛型类型定义或泛型方法定义)【注:泛型参数的装箱和拆箱增加了CLI执行的性能开销。constrained前缀能够在实际调度值类型定义的方法时提高性能,这是通过避免装箱值类型做到的。
类型System.Void永远是不可装箱的。
接口和继承只定义在引用类型上。这样,当值类型定义(见8.9.7章)时能同时指定应该被值类型实现的接口和它继承自的类(System.ValueType或System.Enum),这仅用于装箱值。
CLS 规则3:装箱的值类型不是CLS兼容的
【适当的时候用System .Object,System.ValueType或System.Enum替代装箱类型】
CLS (consumer):不需要导入装箱值类型
CLS (extender):不需要提供语法规则来定义或使用装箱值类型
CLS (framework):不应该将装箱值类型用在它的公共导出方面。
8.2.5 值的相同和相等
有两个定义在所有值对上的二元运算符:相同(identity)和相等(equality)。它们返回Boolean结果,是数学运算符上的等于。也就是说,它们是:
- 反身的 – a op a 为true
- 相称的 – 当且仅当b op a 为true时,a op b为true
- 可传递的 – 如果a op b为true并且b op c为true,那么 a op c为true
另外,虽然相同总是意味着相等,反之却不成立。为理解这些操作之间的差异,考虑3个变量A,B和C,它们的类型是System.String,这里箭头的含义是“是一个对…的引用”:
如果字符序列的位置是相同的(也就是说事实上只有一个字符串在内存里),这些变量的值是完全相同的。如果存储的字符序列是相同的,存储在变量里的值是相等的。这样,变量A和B的值是相同的,变量A和C以及B和C各不相同,所有3个A,B和C的值是相等的。
8.2.5.1 相同
相同操作符被CTS如下定义:
- 如果值有不同的确切类型,那么它们是不相同的。
- 除此以外,如果它们的确切类型是值类型,那么当且仅当值的位序列,位到位都是相同的,它们就是相同的。
- 除此以外,如果它们的确切类型是引用类型,那么当且仅当值的位置是相同的,它们就是相同的。
相同在System.Object通过ReferenceEquals方法来实现。
8.2.5.2 相等
对于值类型,相等运算符是确切的值类型定义的一部分。相等的定义应该服从以下规则:
- 相等应该是一个等价操作符,如上面所定义的。
- 相同应该意味着相等,如前面所述。
- 如果其中一个(或者两个)操作数是装箱类型,相等的判断应该通过以下方式:
- 首先拆箱所有的装箱操作数,然后
- 对结果的值做常用的是否相等的判断。
相同在System.Object通过Equals方法来实现。
【注:尽管两个浮点型指针NaN被IEC 60559:1989定义为比较总是不同,System.Object.Equals的契约要求重载必须满足一个相等运算符的要求。因此,当比较两个NaN时,System.Double.Equals和System.Single.Equals返回true,而这种情况下如按IEC标准,相等运算符应返回False。