预备知识:
在前文中已经介绍了最常用的一大类基本值类型:序数类型。除此以外,还有三类基本值类型;其它值类型都是由其它类型复合而来,这里统一称为复合值类型。
其中一大类基本值类型已经在前文提过了,也就是浮点类型,在Delphi中支持的浮点类型包括:Single, Double, Extended, Currency, Comp, Real48, Real。关于它们的宽度与精度等请自行查阅Delphi的帮助文档,个人认为关于浮点类型没什么好讲的。
还有一大类型是Pascal语言的传统类型——短字符串。今天,它的使用率已经不太高了。由于关于编码的知识还没有介绍,所以这里打算暂时不讲它,等到后面和其它几种字符串一齐讲。
最后一大类基本值类型是指针类型——可能有人会不同意我对指针类型的划分,认为它们应该放在引用类型当中。因为表面上看起来,指针类型除了地址外,还包含有类型的信息;但实际上指针变量中只保存了一个地址,该地址的数据如何解释是由指针类型决定的,换句话说,由程序员去决定如何解释那个地址的数据。这样讲起来可能会有些抽象,我会在不久后对指针进行更详细的介绍——还是老话,千万不要觉得它神秘。
接下来要讲的内容是3种复合类型,包括:集合类型,结构(记录)类型,静态数组类型——这三种值类型的定义都要以其它类型为基础。今天,我打算首先介绍一种相对不复杂的复合类型——集合类型。
在讲接下来的内容之前,这里要先插入一下匿名类型的概念——本打算留到变量部分讲的,但现在看不得不提前进行简单的介绍了。在过去的内容中,我们讲到的新类型的定义,都是声明一个有名称的类型,也就是形如“TNamedType = TypeDefinition;”这种定义方式。回忆一下,已经讲过的类型的定义有以下四种形式:别名类型,重命名类型,枚举类型,序数类型的子类型。匿名类型是说,未将类型定义给出一个明确的名称。在目前四种讲过的定义形式中,别名、重命名两种形式没办法或不允许匿名类型。这个定义可能有些太抽象,不过不要紧,接下来的内容中很快就会接触到。
那么接下来,先来讲解一下以序数类型为基本类型的集合类型吧。集合类型定义的语法为:
TSetType = set of TBaseOrdType;
集合类型是个非常有意思的类型,它要求后基础序数类型的值域不能超过0..255的范围,并从0开始给该类型的每一个元素分配一个位(Bit)。乍一看来,基本类型中只有Boolean、ByteBool、Byte和AnsiChar四种类型可以满足其要求(并且由于不明原因,在定义“set of ByteBool;”时Delphi编译器报出超过256个元素的编译错误,而Free Pascal可以正确编译),好像没多大意义。但是我们可以定义某个子域,例如“THexSet = set of 0..15;”,这时候定义了一个集合类型,用于0到15这一共16个元素的集合。注意这个定义中的“0..15”就是前面提到的匿名类型,它并没有首先指定一个名称,然后再给set定义使用。同理,既可以定义一个枚举类型后,再以之为基础定义一个集合类型,也可以直接在“set of”后跟上一个枚举类型的定义,例如:
TMySet = set of ( ae0, ae1, ae2, ae3 );
这样也能够成功定义一了集合类型。关于集合类型变量的运算,我会在专门的文章中介绍。对于一个集合类型名本身,除了标准的SizeOf、TypeInfo、default这三个类型通用运算符外,并没有其它额外的运算。事实上,除了序数类型外,绝大多数类型都没有这三个运算符以外的运算——所以,一般只研究一个类型的SizeOf运算结果,以后就不再强调这些了。
前面已经说过,集合类型会从0开始,给基础类型的每个元素分配一位。这也就是说,一个集合类型的宽度,取决于基础类型的上限,Ord(High(TBaseOrdType))+1再除以8就是它应该占用的字节数——当然计算机中字节是最小的单位,不可能出现小数部分,所以就是能容纳下这些位的最小字节数。例如有“set of 1..48”,它的基类型的上限是48,因此49/8需要6+1字节,所以它的宽度是7。这里需要注意一个问题,就是一个集合类型的宽度不可能是3,原因在《CPU与基本数据类型基础》文中已经提到过。关于集合类型的讨论暂时就到这里了。
最后再来讨论一下别名与重命名类型定义的涵义。首先,可以自己打开IDE,运行下面一段程序:
program Project1; {$APPTYPE CONSOLE} uses SysUtils; type TOne2Ten = 1..10; TAliasOT = TOne2Ten; TRenameOT = type TOne2Ten; begin try Writeln(Format('TOne2Ten'#9'%p', [TypeInfo(TOne2Ten)])); Writeln(Format('TAliasOT'#9'%p', [TypeInfo(TAliasOT)])); Writeln(Format('TRenameOT'#9'%p', [TypeInfo(TRenameOT)])); except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; Readln; end.
前文已经介绍过,TypeInfo返回一个类型的RTTI地址,除了个别形式外,每种类型都会有它的信息。观察运行结果,很容易发现,别名的地址与基本类型的地址是相同的,而重命名则与之不同,这与前文介绍的结论是相同的:别名并未产生一个新类型,而重命名则定义了一个新类型。那么下面考虑基类型的定义,也就是“TOne2Ten = 1..10;”这句。假如没有左边的内容的话,右边部分将是一个匿名类型。那么匿名类型能否用SizeOf或TypeInfo进行运算呢?这里暂时不回答这个问题,但如果你往SizeOf或TypeInfo里塞了“1..10”的话,编译器肯定会明确告诉你:这货不行。那么能否重命名一个匿名类型呢?这里可以给出明确的答案,“TRenameOT = type 1..10;”是不行的。
这样,可以换一个角度来看待别名与重命名这两种类型的定义方式。当采用别名的定义时,如果右边是一个匿名类型的话,换言之还没有分配明确的类名称的类型,那么编译器会为它生成一个类型的定义;否则,如果是一个已经有名称的类型时,不会生成新的类型名,而会继续改用已有的类型名。我们可以把别名定义看作:编译器用它来定义新类型,并且同时避免重复定义。而当采用重命名的定义时,要求必须是已经有名称的类型。换句话说,它的涵义则是告诉编译器,我明确要在已有类型基础上定义一个新类型——所以匿名类型肯定是不行的。
所以别名与重命名定义,与目前已经介绍过的枚举类型、集合类型,以及后面会介绍的类型定义的涵义是不同的:它们只是用于编译器的对类型名称的处理,用来定义与类型名称有关的内容,而不是用来定义某个类型的实际内容。
最后,留下一道思考题,看看你是否真的理解别名的定义:沿用上个例子中的“TOne2Ten”类型,如果现在再有“TOne2Ten_2 = 1..10;”的定义,那么,TOne2Ten_2与TOne2Ten是同一个类型么?