预备知识:

前面已经介绍过在Delphi中,整数、字符、布尔与枚举类型统称为序数类型,它们之间有很大的共性。这几种中,枚举类型是必须要定义的,RTL并没有提供某个默认的基类型。在本文中,为了省去每次都写的麻烦,如无特殊说明,代码开始的位置一般是type段。一般情况下,枚举类型的基本定义方式如下:

TEnumType = ( identifier_0, identifier_1, ..., identifier_n );

其中,identifier_x称为该类型的元素,符合Delphi的标识符命名方式:在ASCII字符集中,以拉丁字母或下划线开头(_),高版本Delphi支持以其它不在ASCII字符集范围内的Unicode字符开头(如汉字);除首字符外,其它字符在首字符的字符集基础上,再加上阿拉伯数字。枚举类型至少要有1个元素,总量上限并不容易测试。在这种声明方式下,元素的序数值从0开始(也就是说第1个元素的实际值是0),后面每个元素比前一个元素的值大1。在已有序数类型TBaseOrdinal的基础上,假设该序数类型的值域范围是M..N,则可以声明以下三种以该类型为基础的类型:

TAlias = TBaseOrdinal; { 别名 }
TRename = type TBaseOrdinal; { 重命名 }
TSubType = m..n; { 子类型:其中 M<=m<=n<=N }

其中第三种声明是序数类型独有的,前两种声明对所有类型都适用。别名与重命名这两种类型非常类似,区别在于:别名类型完全与基础类型相同,换句话说编译器认为并不存在一个全新的类型;而重命名则是相当于一个新的类似,尽管在重命名类型与基类型变量之间的赋值等情况下,两者都可以互相隐式转换,但在按地址传递参数的时候(也就是用var或out修饰参数时,关于参数会在后面讲到),编译器会认为两者类型不匹配而无法通过编译。在接下来介绍第三种声明之前,首先要介绍一个可以对类型名称使用的运算符:SizeOf。

SizeOf是一个编译期运算符,可以对任何类型名称使用,其运算结果是一个非负整数,用于描述该类型变量的声明在内存中所占用的字节数,一般称之为该类型的宽度。所谓编译期运算符,就是在编译器进行编译时,就已经得出该运算的结果,不需要等到程序运行时才执行。这个概念非常重要,因为很多运算符同时是编译期运算符和运行期运算符,在不同的情况下使用,表现出来的行为不同。例如整数运算,如果该表达式的全部项都是在编译期就能确定值的(如立即数和编译期常量),那么编译器可以直接算出该表达式的值,而不用等到运行期再去计算。

除了Int64/UInt64的宽度是8字节外,其它序数类型的宽度只能是1、2或4字节。对于非枚举类型的序数类型,以及在默认编译条件下的枚举类型,其宽度采用能够容纳下该类型的最小字节数。例如有一个包含300个元素的枚举类型,由于其最小值是0,最大值是299,一个字节最多只能容纳0..255显然不行,所以其结果是两个字节;再比如一个整数的子类型,定义的值域是-3..120,由于一个字节可以表示-128..127范围内的数,所以它的宽度会是1字节。对于整数类型,这个结果很容易判断,但也要注意:枚举类型同样是序数类型,它的宽度也同样由这个原则确定。再300个元素的枚举类型举例,假如声明一个子类型,上下限分别为标号200与20号的元素,也就是说它的值域在19..199之间,所以宽度应该是1字节;假如是第260到第270号元素,值域就是259..269,超过了一个字节能够容纳下的范围,因此宽度是2。这段内容中强调了“默认编译条件下”的枚举类型宽度,当然也就意味着有办法改变其宽度,但这里并不打算介绍,因为改变编译条件之后的行为会更复杂,初学者不易消化。


为了避免初学者的混淆,本文将只介绍单纯与类型有关的东西,而不会涉及变量。接下来要讲的,是其它对类型使用的运算符,这其中很大一部分运算符也可用于变量,但行为会更复杂一些,所以请不要将其对类型的运算与对变量的运算弄混。需要注意的是,针对类型的运算符全部是编译期运算符,在编译期就已经确定值了。

首先要介绍的,是一个只能对类型使用的运算符:TypeInfo。这个运算符几乎可以对任何类型(不限于序数类型)使用,用于返回一个该类型的信息地址(现在我们还暂时不会讲它,因为使用起来相对比较复杂,这里只作为了解性知识介绍一下),也就是一个指向无类型的指针;但它无法对某些情况下的枚举类型使用,否则会返回一个编译器错误。包括哪些情况下的枚举类型呢?从Delphi6开始,引入了一个新的枚举类型的声明语法,即可以在声明枚举类型时,指定元素的整数值:

TEnumType = ( ... identifier_n = x, ... ); { x必须为整数 }

这样有一些好处,例如枚举类型首元素的值可以不必从0开始,比如从1开始或者从-2开始等。接下来元素的值的规则与前面类似,如果未指定一个值的话,则比前一个元素增加1。不过要注意的是,并不是所有这种形式声明的枚举类型都无法进行TypeInfo运算,而只是以下情况:当指定某个元素的值时,与其自然序号的值不同。这句话的意思举例来说就是:假如有一个10个元素的枚举类型,如果不指定值的话,第5个元素的自然序号值就应该是4;现在如果我们给它显式的指定一个值,并且这个值不是4的情况下,就无法进行TypeInfo运算——反之如果给的值还是4的话,实际上就跟没指定值完全没有差别。为什么会产生这样的结果,我会在后面讲类型信息的时候介绍。就我个人来说,我是不使用指定值的声明方式的,如果要定义一个其它的值(如-1),我会采用编译期常量的方式声明,这样用起来既没有差别,又可以使用TypeInfo——当然这只是个人偏好,在这点上完全可以爱怎么来怎么来(如果是我认为不应该用的方式,我会强调的)。

接下来要介绍的,还是一个只能对类型使用的运算符:default。这个运算符是D2009开始新增的,所以如果使用老版本的话会不支持。对一个类型T使用default运算符(default(T)),其返回结果是一个T类型的编译期常量,该常量的所有字节都是用0初始化的。由于一个序数类型的值域的下限可能会大于0(如Tfoo=2..100),所以返回值可能会不在该类型定义的值域内(0超出了Tfoo的范围)。这个行为看起来似乎有些奇怪,但在以后介绍自动管理生存期的引用类型时,我会谈到这个结果产生的原因。

既然刚刚提到了引用类型,那么现在我就先概念性的介绍一下,题目中提到的“值类型”与刚刚提到的“引用类型”之间的差别。可以认为,在声明一个变量时,就在程序的内存中有了一块空间,用于存放这个变量。例如,声明一个局部变量,则变量在栈(Stack)中,而全局变量在全局变量段中。在Delphi中,有些类型虽然用起来很方便,但是它在声明时实际上只声明了一个指针,实际内容在堆(Heap)中——这种类型就是引用类型(如长字符串、动态数组等)。而像目前介绍过的简单类型,如序数类型和浮点类型,都是值类型,在它们声明的时候,保留的空间直指用于存放其值的数据,而不是一个指针。目前简单了解一下就可以了,以后我会详细介绍的。关于栈、堆、全局变量段,参考《Win32编程基础》一文。

接下来,是一组运算符:LowHigh。这组运算符不仅仅是类型运算符,还能对一些类型的变量使用;在应用于类型时,只能对部分类型进行运算,这里介绍对序数类型的运算结果。在作用于序数类型时,这组运算符返回一个值,分别表示该类型的最大值与最小值,返回值的类型与运算的对象类型相同。举个例子,Low(Boolean)的返回值是False,也就是它的下限的序数值是0、类型是Boolean,所以结果就是False。

目前能想到的类型运算符就只有这些了,以后想起来别的我再往本文里加。