我学Delphi心得及笔记----用户自定义数据类型,第三讲

Pascal 语言的一个重要特征是它能自定义数据类型。通过各种类型构造器,你可以定义自己的数据类型,如子界类型、数组类型、记录类型、枚举类型、指针类型和集合类 型。最重要的用户定义数据类型是类(class),类是Object Pascal的面向对象扩展部分,本书不讨论这部分。

你可能会认为其它编程语言也有诸如此类的类型构造器,确实如此,但是Pascal 是第一个完美实现这一理论的语言。至今仍然没有语言有能力定义那么多的数据类型。

命名及不命名的类型

为了后续使用或直接用于变量,需要给自定义类型命名。如果自定义一个命名的类型,你必须将代码放在特定的type区,如下所示:

type// subrange definition   Uppercase = 'A'..'Z';    // array definition   Temperatures = array [1..24] of Integer;    // record definition   Date = record     Month: Byte;     Day: Byte;     Year: Integer;   end;    // enumerated type definition   Colors = (Red, Yellow, Green, Cyan, Blue, Violet);    // set definition   Letters = set of Char; 

你也可使用类型定义构造器直接定义一个变量,此时无需显式命名,如下面的代码:

var   DecemberTemperature: array [1..31] of Byte;   ColorCode: array [Red..Violet] of Word;   Palette: set ofColors;

注意:一般来说,你应该避免使用上述不命名类型,因为你不能把它们作为参数传给例程,也不能用于声名同一类型的其他变量。实际上,Pascal 的类型兼容规则是基于类型名的,而不是基于实际的类型定义。两个类型相同的变量仍有可能是不兼容的,除非他们的类型有完全相同的名字。对于不命名类型,需 要编译器给它分配一个内部名字,因此对于数据结构复杂的变量,要习惯于定义命名数据类型,你一定不会为此白费工夫的。

但是上述自定义类型有什么意义呢?如果你不太熟悉Pascal类型构造器,通过下面内容你会了解它,此外下面还谈到了同类构造器在不同语言中的差 异,因此如果你已熟知上面例举的类型定义,不妨往下读,你会对其中内容感兴趣的。最后,我将演示一些Delphi例子,并介绍一些能动态访问类型信息的工 具。

子界类型
子界类型定义了某种类型的取值范围(因此定名subrange)。你可定义整数类型的子界类型,如取值从1到10或从100到1000,或者定义字符 类型的子界类型,如下所示:
type   Ten = 1..10;   OverHundred = 100..1000;   Uppercase = 'A'..'Z'; 
定义子界类型时,你不需要指定基类的名字,而只需提供该类型的两个常数。所用基类必须是有序类型,定义结果将是另一种有序类型。
如定义一个子界变量,那么赋给该变量的值必须是子界定义范围内的值。下面代码是正确的:
var   UppLetter: UpperCase; begin   UppLetter := 'F'; 
以下代码则是不正确的:
var   UppLetter: UpperCase; begin   UppLetter := 'e'; // compile-time error
以上代码将导致一个编译错误:“Constant expression violates subrange bounds”。
如果代之以下面代码:
var   UppLetter: Uppercase;   Letter: Char; begin   Letter :='e';   UppLetter := Letter; 
Delphi 编译会通过,但在运行时,如果你开启了范围检查编译选项(在工程选项对话框的编译器页设置),你将得到 Range check error (范围检测错误)信息。
注意:建议你在开发程序时开启上述编译选项,以使程序更健壮并易于调试。这样即使遇上错误,你也会得到一个明确的信息而不是难以琢磨的行为。最终完成程序时你可以去掉这个选项,使程序运行得快一些,不过影响很小。因此我建议你开启所有运行时的检测选项,如溢出检查和堆栈检查,甚至提交程序时仍然保留它们。
枚举类型 
 枚举类型又是一种自定义有序类型。在枚举类型中,你列出所有该类型可能取的值,而不是指定现有类型的范围。换句话说,枚举类型是个可取值的序列。见下例:
type   Colors = (Red, Yellow, Green, Cyan, Blue, Violet);   Suit = (Club, Diamond, Heart, Spade); 
序列中每个值都对应一个序号,序号从0开始计数。使用Ord 函数,即可得到一个枚举类型值的序号。例如,Ord (Diamond) 返回值1。
注意:枚举类型有多种内部表示法。缺省时,Delphi 用8位表示法;如果有多于256个不同的值,则用16位表示法。还有一种32位表示法,需要与C、C++库兼容时会用到。使用$Z 编译指令可改变缺省设置,请求更多位的表示法。
集合类型

集合类型表示一组值,该组值由集合所依据的有序类型定义。定义集合的常用有序类型不多,一般为枚举类型或子界类型。如果子界类型取值为1..3,那么基于它的集合类型值可以是1、或2、或3、或1和2、或1和3、或2和3、或取所有3个数、或一个数也没有。

一个变量通常包含该类型对应的一个值,而集合类型可以不包含值、包含一个值、两个值、三个值,或更多,它甚至可以包含定义范围内所有的值。下面定义一个集合:

type   Letters = set of Uppercase; 

现在我可以用上面类型来定义变量,并把原始类型的值赋给变量。为了在集合中表示一组值,需要用逗号将值隔开,最后用方括号结尾。下例显示了多值、单值和空值的变量赋值:

var   Letters1, Letters2, Letters3: Letters; begin   Letters1 := ['A', 'B', 'C'];   Letters2 := ['K'];   Letters3 := []; 

在Delphi中,集合一般用于表示有多种选择的标记。例如下面两行代码(摘自Delphi库)声明了一个枚举类型,其中列出了窗口条上可选的图标,并声明了相应的集合类型:

type   TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);   TBorderIcons = set of TBorderIcon; 
 数组类型

数组类型定义了一组指定类型的元素序列,在方括号中填入下标值就可访问数组中的元素。定义数组时,方括号也用来指定可能的下标值。例如,下面的代码中定义了一个有24个整数的数组:

type   DayTemperatures = array [1..24] of Integer; 

在数组定义时,你需要在方括号中填入一个子界类型的值,或者用两个有序类型的常量定义一个新的子界类型,子界类型指定了数组的有效索引。由于子界类型指定了数组下标值的上界和下界,那么下标就不必象C、C++、JAVA和其他语言那样必须从零开始。

由于数组下标基于子界类型,因此Delphi 能够对它们进行范围检查。不合法的常量子界类型将导致一个编译时间错误;如果选上编译器范围检查选项,那么超出范围的下标值将导致一个运行时间错误。

使用上述数组定义方法,定义一个DayTemperatures 类型的变量如下:

type   DayTemperatures = array [1..24] of Integer;  var     DayTemp1: DayTemperatures;    procedure AssignTemp;   begin     DayTemp1 [1] := 54;   DayTemp1 [2] := 52;   ...   DayTemp1 [24] := 66;   DayTemp1 [25] := 67; // compile-time error

数组可以是多维的,如下例:

type   MonthTemps = array [1..24, 1..31] of Integer;   YearTemps = array [1..24, 1..31, Jan..Dec] of Integer; 

这两个数组建立在相同的核心类型上,因此你可用前面定义的数据类型声明它们,如下面代码所示:

type   MonthTemps = array [1..31] of DayTemperatures;   YearTemps = array [Jan..Dec] of MonthTemps; 

上例的声明把索引的次序前后调换了一下,但仍允许变量之间整块赋值。例如:把一月份的温度值赋给二月份:

var   ThisYear: YearTemps; begin   ...   ThisYear[Feb] := ThisYear[Jan]; 

你也能定义下标从零开始的数组,不过这似乎不太合逻辑,因为你需要用下标2来访问数组第三项。然而,Windows一直沿用了从零开始的数组(因为它是基于C语言的),并且Delphi 控件库也在往这方向靠拢。

使用数组时,你总要用标准函数LowHigh来检测它的边界,LowHigh返回下标的下界和上界。强烈建议使用LowHigh操作数组,特别是在循环中,因为这样能使代码与数组范围无关,如果你改变数组下标的范围声明,LowHigh代码不会受影响;否则,如果代码中有一个数组下标循环体,那么当数组大小改变时你就不得不更新循环体的代码。LowHigh将使你的代码更易于维护、更稳定。

注意:顺便提一下,使用LowHigh不会增加系统运行额外开销。因为在编译时,他们已被转换成常数表达式,而不是实际函数调用。其他简单的系统函数也是这样。

记录类型

记录类型用于定义不同类型数据项的固定集合。记录中每个元素,或者说域,有它自己的类型。记录类型定义中列出了所有域,每个域对应一个域名,通过域名可以访问它。

下面简单列举了记录类型的定义、类型变量的声明以及这类变量的使用:

type   Date = record     Year: Integer;     Month: Byte;     Day: Byte;   end;    var   BirthDay: Date;    begin   BirthDay.Year := 1997;   BirthDay.Month := 2;   BirthDay.Day := 14; 

类和对象可以看作是记录类型的扩展。Delphi 库趋向于用类替代记录类型,不过Windows API中定义了许多记录类型。

记录类型中允许包含variant 域,它表示多个域能公用同一内存区,而且域可以是不同类型(这相应于C语言中的联合union)。换句话说,你可以通过variant 域或说是一组域访问记录中同一个内存位置,但是各个值仍需区别对待。variant类型主要用来存贮相似但又不同的数据,进行与类型映射 (typecasting)相似的类型转换(自从typecasting 引入Pascal,已很少用到这种方法了)。虽然Delphi在一些特殊情况下还在用variant 记录类型,但是现在已经被面向对象技术或其他现代技术代替了。

variant 记录类型的应用不符合类型安全原则,因此不提倡在编程中使用,初学者更是如此。实际上,专家级的编程人员确实需要用到variant 记录类型,Delphi 库的核心部分就用到了这一类型。不管怎样,除非你是个Delphi 专家,否则你应避免使用variant记录类型。

指针

指针是存放指定类型(或未定义类型)变量内存地址的变量,因此指针间接引用一个值。定义指针不需用特定的关键字,而用一个特殊字符,这个特殊字符是脱字符号(^),见下例:

type   PointerToInt = ^Integer; 

一旦你定义了指针变量,你就可以用@ 符号把另一个相同类型变量的地址赋给它。见下例:

var   P: ^Integer;   X: Integer; begin   P := @X;   // change the value in two different ways   X := 10;   P^ := 20;   

如果定义了一个指针P,那么P表示指针所指向的内存地址,而P^表示内存所存储的实际内容。因此,在上面的代码中, P^ 与X相等。

除了表示已分配内存的地址外,指针还能通过New 例程在堆中动态分配内存,不过当你不需要这个指针时,你也必须调用Dispose 例程释放你动态分配的内存。

Delphi 还定义了一个Pointer 数据类型,它表示无类型的指针(就象C语言中的void* )。如果你使用无类型指针,你应该用GetMem 例程,而不是New例程,因为GetMem 例程能用于内存分配大小不确定的情况。

实际上,Delphi 中必须使用指针的情况很少,这是Delphi开发环境一个诱人的优点。虽然如此,若要进行高级编程和完全理解Delphi 对象模型,理解指针是很重要的,因为Delphi 对象模型在幕后使用了指针。

注意:虽然在Delphi中不常使用指针,但是你经常会用一个极为相似的结构--引用(references)。每个对象实例实际上是一个隐含的指针,或说是对其实际数据的引用,利用引用,你能象用其他数据类型一样使用对象变量。

文件类型

另一个Pascal特定的类型构造器是文件类型(file)。文件类型代表物理磁盘文件,无疑是Pascal语言的一个特殊类型。按下面的方式,你可以定义一个新的数据类型:

type   IntFile = file of Integer; 

然后,你就能打开一个与这个结构相应的物理文件、向文件中写入整数、或者从文件中读取当前的值。

Pascal 文件类型的使用很直观,而且Delphi 中也定义了一些控件用于文件保存和装载,以及对数据流和数据库的支持。

Pascal 语言的一个重要特征是它能自定义数据类型。通过各种类型构造器,你可以定义自己的数据类型,如子界类型、数组类型、记录类型、枚举类型、指针类型和集合类 型。最重要的用户定义数据类型是类(class),类是Object Pascal的面向对象扩展部分,本书不讨论这部分。

你可能会认为其它编程语言也有诸如此类的类型构造器,确实如此,但是Pascal 是第一个完美实现这一理论的语言。至今仍然没有语言有能力定义那么多的数据类型。

命名及不命名的类型

为了后续使用或直接用于变量,需要给自定义类型命名。如果自定义一个命名的类型,你必须将代码放在特定的type区,如下所示:

type// subrange definition   Uppercase = 'A'..'Z';    // array definition   Temperatures = array [1..24] of Integer;    // record definition   Date = record     Month: Byte;     Day: Byte;     Year: Integer;   end;    // enumerated type definition   Colors = (Red, Yellow, Green, Cyan, Blue, Violet);    // set definition   Letters = set of Char; 

你也可使用类型定义构造器直接定义一个变量,此时无需显式命名,如下面的代码:

var   DecemberTemperature: array [1..31] of Byte;   ColorCode: array [Red..Violet] of Word;   Palette: set ofColors;

注意:一般来说,你应该避免使用上述不命名类型,因为你不能把它们作为参数传给例程,也不能用于声名同一类型的其他变量。实际上,Pascal 的类型兼容规则是基于类型名的,而不是基于实际的类型定义。两个类型相同的变量仍有可能是不兼容的,除非他们的类型有完全相同的名字。对于不命名类型,需 要编译器给它分配一个内部名字,因此对于数据结构复杂的变量,要习惯于定义命名数据类型,你一定不会为此白费工夫的。

但是上述自定义类型有什么意义呢?如果你不太熟悉Pascal类型构造器,通过下面内容你会了解它,此外下面还谈到了同类构造器在不同语言中的差 异,因此如果你已熟知上面例举的类型定义,不妨往下读,你会对其中内容感兴趣的。最后,我将演示一些Delphi例子,并介绍一些能动态访问类型信息的工 具。

子界类型
子界类型定义了某种类型的取值范围(因此定名subrange)。你可定义整数类型的子界类型,如取值从1到10或从100到1000,或者定义字符 类型的子界类型,如下所示:
type   Ten = 1..10;   OverHundred = 100..1000;   Uppercase = 'A'..'Z'; 
定义子界类型时,你不需要指定基类的名字,而只需提供该类型的两个常数。所用基类必须是有序类型,定义结果将是另一种有序类型。
如定义一个子界变量,那么赋给该变量的值必须是子界定义范围内的值。下面代码是正确的:
var   UppLetter: UpperCase; begin   UppLetter := 'F'; 
以下代码则是不正确的:
var   UppLetter: UpperCase; begin   UppLetter := 'e'; // compile-time error
以上代码将导致一个编译错误:“Constant expression violates subrange bounds”。
如果代之以下面代码:
var   UppLetter: Uppercase;   Letter: Char; begin   Letter :='e';   UppLetter := Letter; 
Delphi 编译会通过,但在运行时,如果你开启了范围检查编译选项(在工程选项对话框的编译器页设置),你将得到 Range check error (范围检测错误)信息。
注意:建议你在开发程序时开启上述编译选项,以使程序更健壮并易于调试。这样即使遇上错误,你也会得到一个明确的信息而不是难以琢磨的行为。最终完成程序时你可以去掉这个选项,使程序运行得快一些,不过影响很小。因此我建议你开启所有运行时的检测选项,如溢出检查和堆栈检查,甚至提交程序时仍然保留它们。
枚举类型 
 枚举类型又是一种自定义有序类型。在枚举类型中,你列出所有该类型可能取的值,而不是指定现有类型的范围。换句话说,枚举类型是个可取值的序列。见下例:
type   Colors = (Red, Yellow, Green, Cyan, Blue, Violet);   Suit = (Club, Diamond, Heart, Spade); 
序列中每个值都对应一个序号,序号从0开始计数。使用Ord 函数,即可得到一个枚举类型值的序号。例如,Ord (Diamond) 返回值1。
注意:枚举类型有多种内部表示法。缺省时,Delphi 用8位表示法;如果有多于256个不同的值,则用16位表示法。还有一种32位表示法,需要与C、C++库兼容时会用到。使用$Z 编译指令可改变缺省设置,请求更多位的表示法。
集合类型

集合类型表示一组值,该组值由集合所依据的有序类型定义。定义集合的常用有序类型不多,一般为枚举类型或子界类型。如果子界类型取值为1..3,那么基于它的集合类型值可以是1、或2、或3、或1和2、或1和3、或2和3、或取所有3个数、或一个数也没有。

一个变量通常包含该类型对应的一个值,而集合类型可以不包含值、包含一个值、两个值、三个值,或更多,它甚至可以包含定义范围内所有的值。下面定义一个集合:

type   Letters = set of Uppercase; 

现在我可以用上面类型来定义变量,并把原始类型的值赋给变量。为了在集合中表示一组值,需要用逗号将值隔开,最后用方括号结尾。下例显示了多值、单值和空值的变量赋值:

var   Letters1, Letters2, Letters3: Letters; begin   Letters1 := ['A', 'B', 'C'];   Letters2 := ['K'];   Letters3 := []; 

在Delphi中,集合一般用于表示有多种选择的标记。例如下面两行代码(摘自Delphi库)声明了一个枚举类型,其中列出了窗口条上可选的图标,并声明了相应的集合类型:

type   TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);   TBorderIcons = set of TBorderIcon; 
 数组类型

数组类型定义了一组指定类型的元素序列,在方括号中填入下标值就可访问数组中的元素。定义数组时,方括号也用来指定可能的下标值。例如,下面的代码中定义了一个有24个整数的数组:

type   DayTemperatures = array [1..24] of Integer; 

在数组定义时,你需要在方括号中填入一个子界类型的值,或者用两个有序类型的常量定义一个新的子界类型,子界类型指定了数组的有效索引。由于子界类型指定了数组下标值的上界和下界,那么下标就不必象C、C++、JAVA和其他语言那样必须从零开始。

由于数组下标基于子界类型,因此Delphi 能够对它们进行范围检查。不合法的常量子界类型将导致一个编译时间错误;如果选上编译器范围检查选项,那么超出范围的下标值将导致一个运行时间错误。

使用上述数组定义方法,定义一个DayTemperatures 类型的变量如下:

type   DayTemperatures = array [1..24] of Integer;  var     DayTemp1: DayTemperatures;    procedure AssignTemp;   begin     DayTemp1 [1] := 54;   DayTemp1 [2] := 52;   ...   DayTemp1 [24] := 66;   DayTemp1 [25] := 67; // compile-time error

数组可以是多维的,如下例:

type   MonthTemps = array [1..24, 1..31] of Integer;   YearTemps = array [1..24, 1..31, Jan..Dec] of Integer; 

这两个数组建立在相同的核心类型上,因此你可用前面定义的数据类型声明它们,如下面代码所示:

type   MonthTemps = array [1..31] of DayTemperatures;   YearTemps = array [Jan..Dec] of MonthTemps; 

上例的声明把索引的次序前后调换了一下,但仍允许变量之间整块赋值。例如:把一月份的温度值赋给二月份:

var   ThisYear: YearTemps; begin   ...   ThisYear[Feb] := ThisYear[Jan]; 

你也能定义下标从零开始的数组,不过这似乎不太合逻辑,因为你需要用下标2来访问数组第三项。然而,Windows一直沿用了从零开始的数组(因为它是基于C语言的),并且Delphi 控件库也在往这方向靠拢。

使用数组时,你总要用标准函数LowHigh来检测它的边界,LowHigh返回下标的下界和上界。强烈建议使用LowHigh操作数组,特别是在循环中,因为这样能使代码与数组范围无关,如果你改变数组下标的范围声明,LowHigh代码不会受影响;否则,如果代码中有一个数组下标循环体,那么当数组大小改变时你就不得不更新循环体的代码。LowHigh将使你的代码更易于维护、更稳定。

注意:顺便提一下,使用LowHigh不会增加系统运行额外开销。因为在编译时,他们已被转换成常数表达式,而不是实际函数调用。其他简单的系统函数也是这样。

记录类型

记录类型用于定义不同类型数据项的固定集合。记录中每个元素,或者说域,有它自己的类型。记录类型定义中列出了所有域,每个域对应一个域名,通过域名可以访问它。

下面简单列举了记录类型的定义、类型变量的声明以及这类变量的使用:

type   Date = record     Year: Integer;     Month: Byte;     Day: Byte;   end;    var   BirthDay: Date;    begin   BirthDay.Year := 1997;   BirthDay.Month := 2;   BirthDay.Day := 14; 

类和对象可以看作是记录类型的扩展。Delphi 库趋向于用类替代记录类型,不过Windows API中定义了许多记录类型。

记录类型中允许包含variant 域,它表示多个域能公用同一内存区,而且域可以是不同类型(这相应于C语言中的联合union)。换句话说,你可以通过variant 域或说是一组域访问记录中同一个内存位置,但是各个值仍需区别对待。variant类型主要用来存贮相似但又不同的数据,进行与类型映射 (typecasting)相似的类型转换(自从typecasting 引入Pascal,已很少用到这种方法了)。虽然Delphi在一些特殊情况下还在用variant 记录类型,但是现在已经被面向对象技术或其他现代技术代替了。

variant 记录类型的应用不符合类型安全原则,因此不提倡在编程中使用,初学者更是如此。实际上,专家级的编程人员确实需要用到variant 记录类型,Delphi 库的核心部分就用到了这一类型。不管怎样,除非你是个Delphi 专家,否则你应避免使用variant记录类型。

指针

指针是存放指定类型(或未定义类型)变量内存地址的变量,因此指针间接引用一个值。定义指针不需用特定的关键字,而用一个特殊字符,这个特殊字符是脱字符号(^),见下例:

type   PointerToInt = ^Integer; 

一旦你定义了指针变量,你就可以用@ 符号把另一个相同类型变量的地址赋给它。见下例:

var   P: ^Integer;   X: Integer; begin   P := @X;   // change the value in two different ways   X := 10;   P^ := 20;   

如果定义了一个指针P,那么P表示指针所指向的内存地址,而P^表示内存所存储的实际内容。因此,在上面的代码中, P^ 与X相等。

除了表示已分配内存的地址外,指针还能通过New 例程在堆中动态分配内存,不过当你不需要这个指针时,你也必须调用Dispose 例程释放你动态分配的内存。

Delphi 还定义了一个Pointer 数据类型,它表示无类型的指针(就象C语言中的void* )。如果你使用无类型指针,你应该用GetMem 例程,而不是New例程,因为GetMem 例程能用于内存分配大小不确定的情况。

实际上,Delphi 中必须使用指针的情况很少,这是Delphi开发环境一个诱人的优点。虽然如此,若要进行高级编程和完全理解Delphi 对象模型,理解指针是很重要的,因为Delphi 对象模型在幕后使用了指针。

注意:虽然在Delphi中不常使用指针,但是你经常会用一个极为相似的结构--引用(references)。每个对象实例实际上是一个隐含的指针,或说是对其实际数据的引用,利用引用,你能象用其他数据类型一样使用对象变量。

文件类型

另一个Pascal特定的类型构造器是文件类型(file)。文件类型代表物理磁盘文件,无疑是Pascal语言的一个特殊类型。按下面的方式,你可以定义一个新的数据类型:

type   IntFile = file of Integer; 

然后,你就能打开一个与这个结构相应的物理文件、向文件中写入整数、或者从文件中读取当前的值。

Pascal 文件类型的使用很直观,而且Delphi 中也定义了一些控件用于文件保存和装载,以及对数据流和数据库的支持。