文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>delphi(4)

delphi(4)

时间:2006-06-10  来源:许我一个信仰

第5章  集合、常数与运行时类型信息编程

定义好的抽象,可以解决许多编程方面的障碍。在面向对象语言中,所谓的抽象就是“创建类”。在某种程度上确实如此。许多项目的失败就是因为创建的类太少,结果形成了一些庞大的类,它们做的事情太多,因而难于维护。

好的抽象也可以在较低层次上定义。像Delphi这样的强类型化编译器、定义在问题域中有具体意义的类型等,都可能是有帮助的。一般的,进行较为精确的类型定义,通常可以更好地定义类的属性。大多数属性的特征可以通过整数和数组进行表达,但相比而言,由内建类型派生而来的集合、范围、常数、数组以及枚举等更有意义。

Object Pascal是一种具有很强的表达能力的面向对象编程语言,它有助于定义在特定问题域的上下文中具有意义的类型。例如,如果只有某个特定范围内的值有意义,就定义范围、集合或枚举类型来命名这些值所代表的数据。本章示范了如何使用Object Pascal中的这些概念,以有助于定义好的抽象。这些技术可以使您的代码具有更强的可读性,而且比使用内建数据类型所需的错误检查代码要少。

5.1  不可变常数

常数很好用。常数是现存的最为可靠的代码之一。定义常数之后,无论如何其值都是可以依赖的。不用担心,不存在偶然或有意的误用。在Delphi中有许多方法来使用常数,以使得代码更加可靠。

注意:Delphi支持类型化常数,它们的值是可变的。关于可赋值的常数,更多的信息请参见5.1.3节“使用const创建静态本地变量”。

5.1.1  全局与本地常数

当变量定义在本地作用域中时,可以访问该作用域的代码均可使用该变量。临时使用全局或本地变量可能导致有害的问题,特别是对于多线程应用程序,其中一个线程可能依赖于某个值,而另外一个线程正在改变该值。如果一个值需要保持不变,则应该用const来表示。全局常数是定义在单元的接口部分的常数。而本地常数是定义在实现部分的常数。

另外,可能会在许多地方重用的值应该定义为常数。假定Pi的值在您的整个程序中都是有意义的,则应在接口部分将名字Pi作为常数引入,并将其值初始化为具有正确的有效数字位数的Pi值,以满足您的需要。

注意:新的单元ConvUtils.pas包含有数以百计的常数和转换单元。尽管它包含了秒差距与米的转换常数值,但并未包含Pi的常数值。System.pas单元中包含了函数Pi,返回Pi值的extended类型的浮点值。

我们的目标是在尽可能狭窄的作用域中定义常数。如果一个常数只在过程块中需要,那么该过程就是合适的作用域。使作用域变窄背后的思想在于,要尽量减少使用代码的程序员在理解代码目的时所需要进行思考的事物的数目。常数的语法部分依赖于其所定义的上下文。本地、全局、过程常数的通常形式如下:

const name = value;

或,

const name : type = value;

const是关键字,表示其后是常数。对于一个常数列表中的所有常数,仅需要键入const一次。例如,在实现部分定义三个常数,如下所示。

implementation

const

I : Integer = 3;

S = 'Bachman Turner Overdrive';

F : Double = 4000000000000.0;

有许多途径来使用常数,可以使得程序更加可靠。对于常数的所有变体的语法规则,请察看上下文帮助,在索引中查找grammar。更多的例子见下文。

5.1.2  常数参数

当过程不应改变某个参数的值时,应把该参数声明为常数参数。如果包括了const限定符,可以保证该值不被改变。保证总是难于得到,因此能够得到保证确实不错。常数参数可以有默认值。下面的代码演示了具有默认值的常数参数。

procedure DisplayBandName( const Value : String = 'R.E.O.' );

begin

ShowMessage( Value );

end;

Procedure SomeProc;

const

BTO = 'Bachman Turner Overdrive';

begin

DisplayBandName;

end;

DisplayBandName过程中定义了一个具有默认值的参数。如果不传递参数,Value参数的值将是‘R.E.O.’。如果把常数BTO传递给DisplayBandName,那么ShowMessage函数将显示Bachman Turner Overdrive。常数参数的存在保证了调用的方法不会在无意中改变传递的参数值。使用const要远胜于希望和祈祷。

5.1.3  使用const创建静态本地变量

定义在过程中的变量在栈上分配内存空间。常数通过编译嵌入到代码中,只存在于所定义的过程中。当过程调用或退出时,栈内存空间像手风琴一样来回伸缩。通常,在过程中引入的名字具有过程作用域。即,该名字和值只在所定义的作用域中可访问。有时,您可能需要各种占位符,即只在过程作用域可访问的名字,而在过程返回后依然保持其值。C和C++称之为静态变量。Delphi用可赋值常数来产生同样的效果。

使用下面的语法您可以定义一个变量,它看上去是常数,但实际上是可变的静态变量。

Procedure MutableConst;

const

I : Integer = 0;

begin

Inc(I);

ShowMessage(IntToStr(I));

end;

// ...

for I := 0 to 3 do MutableConst;

在上面的MutableConst过程中定义一个类型化的可赋值常数,常数的值在该过程的各次调用之间仍然可以保持。最后一行的for语句调用MutableConst四次,最后一次调用在ShowMessage的对话框中显示值为4。默认情况下,类型化常数是可赋值的。可以通过$J+编译器指令进行改变;或者在Project Options对话框中的Compiler属性页中改变Assignable typed constants复选框,如图5.1所示。

图5.1  默认情况下,类型化常数是可赋值的,并且可以在对其所定义的过程的后续调用之间维持其值。要使其不可赋值,对Project Options对话框中的Compiler属性页的Assignable typed constants复选框取消选定即可。默认情况下,该复选框是选定的

可赋值类型化常数使得可以在过程中定义占位符,每次该过程调用时都可以维护该值。通过使用可赋值类型化常数,可以模拟静态特性(有关静态特性的更多知识,请阅读第7章)。

5.1.4  数组常数

对您的武器库来说,数组常数是另外一项可以添加的工具。也许您不会每天都用到数组常数,但在日常编程中确实有一些数组常数的例子。考虑下列例子。

Procedure ArrayExamples;

const

DaysOfWeek : array[1..7] of string = ('Sunday', 'Monday', 'Tuesday',

'Wednesday', 'Thursday', 'Friday', 'Saturday' );

MonthsOfYear : array[1..12] of string = ('January', 'February', 'March',

'April', 'May', 'June', 'July', 'August','September', 'October',

'November', 'December' );

EXAMPLE1 = 'February 12, 1966 occurred on a %s';

EXAMPLE2 = 'The fourth month is %s';

var

Output : string;

Day : Integer;

begin

Day := DayOfWeek( StrToDate('02/12/1966'));

Output := Format( EXAMPLE1, [DaysOfWeek[Day] ] );

ShowMessage( Output );

Output := Format( EXAMPLE2, [ MonthsOfYear[4] ] );

ShowMessage(Output);

end;

DaysOfWeek数组包含了7个元素,都是字符串。MonthsOfYear数组包含了12个元素,也都是字符串。两个数组都初始化为常数数组。Begin块语句的第2行使用星期中某天对应的数字来索引数组。第1次调用ShowMessage过程的输出为‘February 12, 1966 occurred on a Saturday’。Begin块语句的第4行对该年的月份执行了一个简单的操作。

当然您也可以把上面的代码写成由嵌套if条件测试组成的case语句,但常数数组在紧凑的操作中生成了优化而更小的代码。考虑下一个例子,其中用if条件测试来比较激活状态以设置控件的背景颜色,也提供了用数组实现的相同功能的代码。

if( Edit1.Enabled = False ) then

begin

Edit1.Enabled := True;

Edit1.Color := clWhite;

end

else // True

begin

Edit1.Enabled := False;

Edit1.Color := clBtnFace;

end;

上面的代码计算了TEdit控件(随机选定)的Enabled状态,对该状态取反,并相应地设置颜色。该代码实用而直接,但使用常数数组可使之更为有效。使用常数数组的修订版本如下。

const

Colors : array[Boolean] of TColor = (clBtnFace, clWhite);

begin

Edit1.Enabled := Not Edit1.Enabled;

Edit1.Color := Colors[Edit1.Enabled];

end;

常数数组使得我们可以将代码缩减到原来的五分之一。使用单目not操作符来执行Enabled状态的切换,用Enabled特性的布尔值来索引常数数组Colors。在使用布尔值作索引时,False的值较小。上面的代码更加紧凑,既小且快。下一节的内容是有关记录常数的,阅读时请注意其中一个用到记录数组常数的例子。

5.1.5  记录常数

记录常数是类型为记录的常量数据。一个很普遍的记录是TPoint。TPoint在笛卡尔坐标系中定义了两个坐标。TPoint在Windows.pas单元中如下定义:

TPoint = record

x: Longint;

y: Longint;

end;

要初始化常量记录,需要以fieldname : value的形式指定每个字段,每个字段的名字与值用冒号分隔。这里有一个例子。

const Point : TPoint = (X:100; Y:100);

常量记录数组需要初始化每个数组元素,对于构成记录值的字段,将名字和值对的集合用括弧括起来。这里是一个由四个点构成的数组。

const

Points : array[0..3] of TPoint = ((X:10;Y:10), (X:10;Y:100), (X:100;Y:100),

(X:100; Y:10));

Procedure DrawRect( const Points : Array of TPoint );

var

I : Integer;

begin

Canvas.PenPos := Points[0];

for I := Low(Points) to High(Points) do

Canvas.LineTo( Points[I].X, Points[I].Y );

Canvas.LineTo( Points[0].X, Points[0].Y );

end;

由于并未考虑监视器屏幕的高宽比,该数组只是粗略地定义了如图5.2所示的正方形。数组Points被传递给代码中的DrawRect过程,用以生成如图5.2所示的正方形。

图5.2  利用LineTo方法和TPoint记录组成的

       数组,在窗体的画布上所画出的正方形

通过将一些基本的Object Pascal术语联合起来,可以很容易地创建很多种常量数据,用于代表各种各样的信息。使用记录和数组常数,可以使您的代码更具有表现力。通过把精确定义的数据映射到问题域的信息,对代码控制得越好,管理数据所需的代码就越少。

5.1.6  过程常数

过程常数是用const修饰的名字,其数据类型为过程类型。一般来说,过程类型只是指向过程的指针,它使得可以把过程和函数赋值给类型与其相匹配的变量和参数。过程类型的更多知识可以阅读第6章。一个过程类型的例子就是定义在classes.pas单元中的TNotifyEvent。下面是classes.pas的摘录,给出了TNotifyEvent的定义。

type TNotifyEvent = procedure (Sender: TObject) of object;

从列出的代码可以看出,TNotifyEvent类型是由过程组成的,它们有一个TObject类型的参数,名字为Sender。类型定义结尾的of object表示TNotifyEvent类型是被称为方法指针的特定过程类型。

TNotifyEvent看起来很熟悉,因为它就是Object Inspector中许多事件特性的类型。双击空白窗体,Delphi将为窗体的OnCreate事件特性创建如下的空方法定义。

procedure TForm1.FormCreate(Sender: TObject);

begin

end;

注意:按照惯例,Delphi使用On前缀表示该特性为事件特性。On暗示着对动作的响应,即事件。

代码中的TForm1.部分表示该方法属于TForm1类。FormCreate表示它是OnCreate事件的处理程序,将类名和方法名从定义中剥离,余下的就是procedure (Sender:TObject),恰好可以与TNotifyEvent匹配。在Delphi中选择OnCreate事件特性,按键F1,将显示CustomForm.OnCreate的上下文帮助。帮助文档清楚地指出,OnCreate定义为特性OnCreate : TNotifyEvent; 即类型为TNotifyEvent的特性。

5.1.7  指针常数

无论使用任何语言,我们的思维都只是受限于用以表达思想的语言以及我们对它掌握的熟练程度。一些术语可能不像其余的那样常用。指针常量就是其中的一个。在日常的Windows应用编程中,指针可能不太常用,不过Delphi并不限制您只能编写典型的Windows风格的程序。指针常量就是指向特定地址的指针。下面的代码琐碎而令人困惑,但它演示了与指针常量有关的必要机制。

type

TDateTimeFunc = function : TDateTime;

const

NowP : Pointer = @SysUtils.Now;

var

MyNow : TDateTimeFunc absolute NowP;

第2行定义了一个过程类型,它是指向函数的指针,返回TDateTime值。常量指针被初始化为SysUtils.pas中定义的Now函数地址。变量MyNow类型为TDateTimeFunc(在类型部分定义),将编译为SysUtils.Now函数的绝对地址。

5.1.8  用于初始化常量的过程

在前面关于过程常数的章节中,您已经知道过程类型可用于定义常数并初始化为某一特定的过程。对于前一节中的例子,无须使用Pointer即可定义对SysUtils.Now函数的常量引用。

type

TDateTimeFunc = function : TDateTime;

const

ConstNow : TDateTimeFunc = SysUtils.Now;

调用ConstNow与调用SysUtils.Now具有相同的效果。由于Delphi支持过程类型,因此用这种形式定义指向过程的变量或常数更为可取。无论对于哪种类型,均可使用Pointer来指向一个特定的内存地址。

5.2  枚举的使用

枚举是代表值的名字列表。如果使用枚举更有意义,那么它比内建数据类型更为可取。枚举的一个完美的例子是TFontStyle。有四种基本的字体风格:黑体、斜体、加下划线的、加删除线的。很清楚,可以用整数来存储字体风格的状态,但对特定风格进行列表更有意义。graphics.pas单元如下定义了TFontStyle类型。

type TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeOut);

定义TFontStyle使得程序员可以声明TFontStyle类型的变量,保证了所有赋予TFontStyle类型变量的值都是有效的,而且不必进行访问检查和错误检查。由于编译器是强类型的,只有四个可能值之一才能赋予TFontStyle变量。简言之,所有困难的工作都由编译器来做,因而代码对于程序员更加可读。

5.2.1  用枚举定义数组边界

按照经验规则,相对于原始的数组,TList和TCollection更为可取。回顾使用对象而不是数组来存储数据的原因:表和集合类可以动态地伸缩,其中包括了范围检查及其他一些功能,如排序和查找等。使用数组则需要实现所有这些功能。

有些情况下,您可能需要使用数组。除去索引范围检查的一种方法是:使用枚举作为索引的类型,从而使数据的范围精确化。这里有一个例子演示了TNote类型,它使用Windows API函数Beep来播放一个音符(音符的频率和长短是模拟的)。

type

TNote = (doDo, doRe, doMi, doFa, doSo, doLa, doTi, doDo2 );

Procedure PlayNote( Note : TNote );

const

DoReMi : array[TNote] of Integer = (

500, 600, 700, 800, 900, 1000, 1100, 1200 );

begin

Windows.Beep( DoReMe[Note], 750 );

Sleep(250);

end;

Procedure PlayNotes;

var

I : TNote;

begin

for I := Low(TNote) to High(TNote) do

PlayNote( I );

end;

第二行定义了一个枚举类型TNote,包含八个元素。PlayNote过程使用Low和High函数得到值的范围,然后对每个元素进行迭代。可以注意到索引I定义为TNote类型而不是整数。每次循环时,都以当前枚举值为参数调用PlayNote过程。PlayNote过程参数为TNote类型,过程中定义了一个索引为TNote类型的常量数组,并使用Windows API函数Beep(并非Delphi所提供的版本)来播放每个音符对应的频率。请注意并不需要范围检查,因为对于数组DoReMi,所有可能的TNote值都是可行的,因为它们都受到枚举范围的限制。

定义枚举和其他精确化的类型具有累积效应。需要调试和测试的代码会逐渐减少,您的代码将更有效地运行。

5.2.2  预定义枚举类型

有许多预定义的枚举类型,实际上,枚举类型太多以至于无法全部涵盖。这种类型的参考手册最好留给在线帮助文档去作。如果您已经习惯于用以控制组件行为的枚举类型,就可以更加出色地控制VCL。以TControlStyle为例。所有的控件都具有ControlStyle特性,该类型定义为枚举值的集合(有关集合操作和定义,更多的信息请参见下一节)。

type TControlStyle = set of (csAcceptsControls, csCaptureMouse,

csDesignInteractive, csClickEvents, csFramed, csSetCaption, csOpaque,

csDoubleClicks, csFixedWidth, csFixedHeight, csNoDesignVisible,

csReplicatable, csNoStdEvents, csDisplayDragImage, csReflector,

csActionClient, csMenuEvents);

ControlStyle特性可以具有零个、一个或多个定义在上述枚举中的值。例如,如果ControlStyle具有csAcceptsControls值,则该控件就可以拥有其他的控件。例如窗体可以拥有控件,但默认情况下,栅格控件是无法拥有其他控件的。

对于已有的控件,其行为是由作者定义的。但您总是可以子类化一个控件,根据您的需要调整其行为。例如,下面的类定义了TControlGrid类型,示范了如何在栅格单元中拥有其他控件(如图5.3所示)。

图5.3  可以在栅格单元中拥有其他控件的栅格控件

TControlGrid = class(TStringGrid)

private

FButton : TButton;

protected

Procedure WMCommand( var Message : TWMCommand ); Message WM_COMMAND;

Procedure OnClick( Sender : TObject );

Procedure Paint; override;

public

constructor Create( AComponent : TComponent); override;

destructor Destroy; override;

end;

 

注意:如果在设计时可以在栅格上绘制出控件,TControlGrid类就可以更有用。使用上面以及下面列出的代码,即可完成该栅格控件,从而可以在设计时和运行时动态地拥有控件。关于栅格组件的演示,参见http://www.softconcepts.com/demos/componentgrid.htm。演示会下载一个ActiveX控件到您的PC,该控件示范了一个全功能的栅格控件。

该类包含一个private字段FButton,用于示范拥有控件的功能。protected message方法重载了WM_COMMAND的信息处理程序。这使得被拥有的控件可以接收到发送给它们的消息,但TStringGrid并未如此,因为在原来的设计中TStringGrid是无法拥有父控件的。OnClick事件处理程序用于演示TButton控件对用户输入的响应。构造函数和析构函数中重载了ControlStyle特性,并对TButton控件进行分配和释放。

{ TControlGrid }

constructor TControlGrid.Create(AComponent: TComponent);

begin

inherited Create(AComponent);

ControlStyle := ControlStyle + [csAcceptsControls];

FButton := TButton.Create(Self);

FButton.Parent := Self;

FButton.BringToFront;

FButton.OnClick := OnClick;

Repaint;

end;

Procedure TControlGrid.Paint;

begin

inherited Paint;

FButton.Visible := (LeftCol = 1) and (TopRow = 1);

FButton.Enabled := FButton.Visible;

FButton.BoundsRect := CellRect( 1, 1 );

end;

destructor TControlGrid.Destroy;

begin

FButton.Free;

inherited;

end;

procedure TControlGrid.OnClick(Sender: TObject);

begin

MessageDlg('Greetings Earthlings!', mtInformation, [mbOK], 0);

end;

Procedure TControlGrid.WMCommand( var Message : TWMCommand );

begin

inherited;

if( ControlCount > 0 ) then

FindControl( Message.Ctl ).Dispatch(Message);

end;

构造函数ControlGrid调用TStringGrid的构造函数,创建按钮,并为按钮的OnClick事件设置处理程序。Paint方法被重载,用于在单元1中绘制该单元的控件(1单元可见时)。对于动态的控件,跟踪该控件属于哪一个单元是必要的。析构函数释放了按钮,并调用TStringGrid的析构函数。当单击按钮时,OnClick方法将显示友好的问候。最后,WMCommand方法响应所有的消息,首先调用继承的消息处理程序,然后对拥有的控件分发消息(消息处理程序的更多知识请阅读第6章)。

在逐渐掌握Delphi的体系结构之后,无论编写简单或还是复杂的控件,都可以利用已有类的属性进行简化,这样可以使程序更加灵活、实用。

5.2.3  用于枚举类型的过程

有一些函数是专门设计用于枚举类型的。表5.1列出用于枚举类型的过程,并描述了每个过程所执行的操作。

表5.1  用于枚举类型的过程

过程

描述

Ord

返回整数,表示与其位置相关的枚举值

Pred

返回在传给函数的值之前的枚举值

Succ

返回传给函数的值的下一个枚举值

High

返回最大的枚举值

Low

返回最小的枚举值

枚举是有序类型,基于在类型定义中的出现次序,枚举元素自动地分配连续值,从第一个位置的0开始到最后一个位置的n-1结束。例如,可以使用High和Low函数得到枚举的上界和下界。如果包含了运行时类型信息(RTTI),则枚举的符号名也可以得到。下面列出的程序演示所有的五个枚举函数,以及如何包含RTTI信息。

uses typinfo;

{$M+}

type

TEnums = ( Enum0, Enum1, Enum2, Enum3, Enum4);

{$M-}

procedure ShowEnum( Enum : TEnums );

const

MASK = '%s=%d';

var

Name : String;

Value : Integer;

begin

Name := GetEnumName( TypeInfo(TEnums), Ord(Enum) );

Value := GetEnumValue( TypeInfo(TEnums), Name );

ShowMessage( Format( MASK, [Name, Value] ));

end;

procedure TestEnumerated;

begin

ShowEnum( Enum3 );

ShowEnum( Pred( Enum3 ));

ShowEnum( Succ( Enum3 ));

ShowMessage( IntToStr(Ord( Enum4 )) );

ShowEnum( Low(TEnums));

ShowEnum( High( TEnums ));

end;

注意:尽管Delphi本身也使用了GetEnumName和GetEnumValue函数,但由于某些原因,它们并未包括在上下文相关帮助中。RTTI用于特性的读写,以及OpenTools API(关于OpenTools API,请参考附录A)。

第一行的uses语句表示应包括typinfo单元。该单元包括了与运行时类型信息相关的过程,其中就有在本例中用到的GetEnumName和GetEnumValue。第四行定义了枚举类型TEnums,该定义包含在{$M+}和{$M-}编译器指令之间。{$M}指令指示编译器对TEnums类型加入运行时类型信息。

第六行开始的ShowEnum过程示范了如何使用typinfo.pas中所定义的GetEnumName和GetEnumValue过程。这两个过程的第一个参数是指向TTypeInfo记录的指针。如代码所示,将枚举类型的名字传递给TypeInfo函数将返回指向类型信息记录的指针。GetEnumName中的第二个参数是枚举类型中某个特定元素所对应的有序数值。GetEnumValue则根据类型信息记录和枚举元素的名字返回对应的有序数值。TestEnumerated过程的输出如下:

ShowEnum( Enum3 ); // outputs Enum3=3

ShowEnum( Pred( Enum3 )); // outputs Enum2=2

ShowEnum( Succ( Enum3 )); // outputs Enum4=4

ShowMessage( IntToStr(Ord( Enum4 )) ); // outputs 4

ShowEnum( Low(TEnums)); // outputs Enum0=0

ShowEnum( High( TEnums )); // outputs Enum4=4

某些低层的VCL过程使用了运行时类型信息。在创建组件和枚举时,运行时类型信息特别有用。枚举可以使代码更加健壮、富于表现力、可读性好。

5.3  集 合 操 作

集合通常表示一组相关的事物,如一组塔珀家用塑料制品或一组高尔夫球棍。集合操作是人类最早了解的数学知识之一(至少在美国和西欧是这样)。至少有三十年了,Sesame Street一直教着这首歌“Which one of these things is not like the other? Which one of these things just doesn’t belong?”即,哪些是集合的成员而哪些不是?集合在实际的世界中是很常见的,因此,很自然的,在抽象世界中应存在这样的术语,使得开发者可以表达集合的概念并对其进行算术操作。在Object Pascal中确实如此,我们只需将自己的理解映射到Delphi中的集合实现即可。

5.3.1  理解集合以及set of语句

集合是同一有序类型的值的聚集。集合的例子有:所有整数的集合、某个枚举类型中所有元素的集合或彩虹中所有元素的集合。在Object Pascal中的集合的大小限定到一个字节,这意味着一个特定集合的基类型必须限制到少于256个元素,而其有序值必须在0到255之间。集合的值的范围是其基类型的幂集,幂集就是包括空集在内一个集合的所有可能的子集的集合。定义集合的语法是:SetType -> SET OF OrdinalType,其中OrdinalType定义为:OrdinalType -> (SubrangeType | EnumeratedType | OrdIdent)。就是说,集合定义在单元的类型部分,将一个名字与子界类型、枚举类型或有序类型的集合联系起来即可。OrdinalType所涉及的各种类型示范如下。

TRangeSet = set of 0..255;

TCharSet = set of char;

TPrimaryColors = set of (pcRed, pcBlue, pcGreen);

TRangeSet示范了一个由整数构成的子界集合,其值由0到255。TCharSet定义了由三原色构成的枚举值的集合。定义集合类型后,则集合实例都是一些子集,其中包含了一些该类型的元素。

5.3.2  使用集合构造器

set of语句定义了一个类型,其中包括从某个有序类型而来的一个有限范围内的值。集合类型的变量可以是受类型定义约束的幂集的任一元素。要初始化集合类型的实例,可以使用集合构造器。

集合构造器由[ ]标识,其中包含一些由逗号或..分隔的值。考虑上一节的TCharSet集合。要构造一个包含大写字母的TCharSet变量,使用如下代码即可。

var

UpperCaseChars : TCharSet;

begin

UpperCaseChars := ['A'..'Z'];

// ...

end;

变量UpperCaseChars是TCharSet的一个子集,初始化时包含了所有的大写字母。由于UpperCaseChars定义为变量,因此可以向集合添加成员。要使UpperCaseChars只包含大写字母字符,可以将其定义为常数并使用$J-编译器指令使该常量不可赋值,或者定义一个只包含大写字母的集合类型。

const

{$J-}

UpperCaseChars : TCharSet = ['A'..'Z'];

{$J+}

或者

type

TUpperCaseChars = set of 'A'..'Z';

提示:默认情况下,Delphi中的类型化常数是可写的。要限制UpperCaseChars只包含从‘A’到‘Z’的字母,可以重新定义集合类型或者把可写类型化常数用编译器指令包裹起来,如下所示:
{$J-} const UpperCaseChars : TCharSet = ['A'..'Z']; {$J+}。

现在,UpperCaseChars是个只包含字符A到Z的不可赋值的常数,而TUpperCaseChars类型的范围则限制为字符A到Z。可定义TUpperCaseChars类型的变量,例如:

var

UpperCaseChars : TUpperCaseChars;

这样UpperCaseChars的值就隐含地限制到了从‘A’到‘Z’。回想一下,默认情况下Delphi中的类型化常数是可写的(请参考上面的提示,其中简短地讨论了对代码的可能修改,从而使其目的性更强)。为包括大写字母和小写字母,扩展上面的集合构造器以包含小写字母。

const

AlphabeticChars : TCharSet = ['A'..'Z', 'a'..'z'];

现在TCharSet变量AlphabeticChars包含了所有大写和小写的字母字符。为简化集合代数,有许多操作符可以执行集合算术运算。

5.3.3  集合操作符

表5.2包含了集合的算术操作符的完整列表,并描述了对集合进行的操作。所有的集合运算,其结果或者为布尔值,或者为新的子集。

表5.2  集合操作符与结果类型,除了in以外,所有的集合操作符的两个操作数都是集合

操作符

操作

结果

例子

+

集合

Set1 + Set2

-

集合

Set1 – Set2

*

集合

Set1 * Set2

<=

是…的子集

布尔值

Set1 <= Set2

>=

是…的超集

布尔值

Set1 >= Set2

=

相等

布尔值

Set1 = Set2

<>

不等

布尔值

Set1 <> Set2

in

是…的成员

布尔值

Ordinal in Set1

下面列出的代码示范了对四个集合执行的集合操作,它们都定义为字符集合的子集。

type

TCharSet = set of char;

const

A : TCharSet = ['A'..'M', 'R', 'S', 'U'];

B : TCharSet = ['B', 'G', 'H', 'L'..'Z'];

SubsetA : TCharSet = ['A'..'G'];

SupersetA : TCharSet = ['A'..'M', 'R', 'S', 'U', 'V'];

 

Procedure TForm1.DisplayResultset( OperationName : String; const

CharSet : TCharSet );

var

I : Char;

Count : Integer;

begin

Memo1.Lines.Add( '***' + OperationName + '***' );

Count := 0;

for I := Low(Char) to High(Char) do

if( I in CharSet ) then

begin

Memo1.Lines.Add(I);

Inc(Count);

end;

Memo1.Lines.Add( '*** Elem Count: ' + IntToStr(Count) + ' ***' );

end;

 

Procedure TForm1.SetTests;

const

BOOLS : array[Boolean] of String = (' is False', ' is True' );

begin

Memo1.Clear;

DisplayResultSet( 'union', A + B );

DisplayResultSet( 'difference', A - B );

DisplayResultSet( 'intersection', A * B );

Memo1.Lines.Add( 'A < SubSetA (Not A >= SubSetA)' +

BOOLS[ Not (A >= SubSetA)] );

Memo1.Lines.Add( 'A > SuperSetA (Not A <= SuperSetA)' +

BOOLS[ Not (A <= SuperSetA) ] );

Memo1.Lines.Add( 'A <= SupersetA' + BOOLS[ A <= SuperSetA ] );

Memo1.Lines.Add( 'A >= SubsetA' + BOOLS[A >= SubSetA] );

Memo1.Lines.Add( 'A = B' + BOOLS[A = B ] );

Memo1.Lines.Add( 'A <> B' + BOOLS[A <> B] );

Memo1.Lines.Add( '''A'' in B' + BOOLS[ 'A' in B ] );

end;

类型声明部分定义了一个由字符组成的集合类型。常数声明部分包含了四个集合的定义,它们是字符集合的子集。集合A和B是不同的集合,SubsetA初始化为集合A的子集,而SuperSetA初始化为集合A的超集。DisplayResultSet方法使用in操作符来测试某个特定的字符是否是结果集合的成员。例子的输出如下。

·      A与B的并集A+B是所有大写字母字符的集合,因为所有的大写字母字符或者在A中或者在B中。

·      A与B的差集A-B包含字母A、C、D、E、F、I、J、K,因为A的这些元素不是B的成员。B-A则得到一个不同的结果集合。

·      A与B的交集A*B包含字母B、G、H、L、M、R、S、U,因为A和B中均包含这些元素。

·      如果SubSetA是A的子集,则用Not (A >= SubSetA)实现的A < SubSetA结果为False。

·      如果SuperSetA是A的超集,则用Not (A <= SuperSetA)实现的A > SuperSetA结果为False。

·      A <= SuperSetA的结果为True,因为SuperSetA中包含所有A中的元素以及‘V’。

·      A >= SubSetA的结果也是True,因为SubSetA中只包含A的部分元素。

·      A = B结果为False,因为A中的所有元素都不在B中。

·      A <> B结果为True(见前一项条件测试A = B)。

·      字符‘A’不是集合B的成员,因此‘A’in B结果为False。

对谓词与命题的演算,定义了用于集合的逻辑操作和谓词语句的代数规则。但不要假定用户也上过离散数学的课程,应该考虑把繁复的集合操作分解为对中间结果进行运算的单一的集合操作。

集合代数演算

基本的四个代数定律对集合逻辑也是适用的。这意味着集合具有传递性,即如果A = B而且 B = C则A = C;集合具有对称性,即A = A;集合具有交换性(对集合的差不适用),即A + B = B + A;集合具有分配性,A * B + A * C = A * ( B + C )。如果需要,可以用这四个基本的数学定律简化复杂的集合等式。下列代码示范了集合的分配律、对称律以及交换律,这些定律都是对基类型为byte的数集验证的。

type

TSet = set of byte;

const

Set1 : TSet = [1, 2, 3, 4];

Set2 : TSet = [3, 4, 5, 6];

Set3 : TSet = [5, 6, 7, 8];

 

procedure DisplayResult( ASet : TSet );

var

I : Byte;

begin

for I := Low(Byte) to High(Byte) do

if( I In ASet ) then

Form1.Memo1.Lines.Add( IntToStr(I));

end;

 

procedure SetTest;

const

BOOLS : array[Boolean] of string = ('False', 'True');

var

ResultSet : TSet;

begin

// Distributive Law

// ResultSet := (Set1 * Set3) + (Set2 * Set3);

ShowMessage( BOOLS[ Set3 * (Set1 + Set2) = (Set3 * Set1) + (Set3 * Set2)] );

ResultSet := Set3 * (Set1 + Set2);

DisplayResult( ResultSet );

// reflexive A + B = B + A

ShowMessage( BOOLS[Set1 + Set2 = Set2 + Set1] );

ShowMessage( BOOLS[Set1 - Set2 = Set2 - Set1] );

// symmetric A = A

ShowMessage( BOOLS[Set1 = Set1] );

end;

注意:SysUtils.pas包含了两个方法StrToBool和BoolToStr,可以对字符串与布尔值之间进行转换。

代码开头的类型声明将TSet类型定义为基类型为byte的集合。三个集合Set1、Set2和Set3定义为TSet类型的集合,包含了小于10的一些特定的单字节整数(这里有意地使集合的成员较为简单,因此您可以在头脑中完成集合操作)。过程DisplayResult接受一个TSet类型的参数,并对参数中所有的单字节整数进行迭代,直到将集合的所有成员都添加到TMemo类型的变量。过程SetTest中定义了一个以布尔值为索引的常量数组,用来提供与布尔值相对应的字符串值。第一组语句通过演示Set3 * (Set1 + Set2) = (Set3 * Set1) + (Set3 * Set2),示范了集合的分配律。第二组语句对集合并操作示范了交换律,并显示True,但ShowMessage对话框对Set1 – Set2 = Set2 – Set1显示了False。最后,对称律是显然的。

集合成员测试

in操作符用于测试集合的成员资格。它是个双目操作符。其左侧是有序类型值,右侧是集合。语法形如ordinal In Set,可读作“该有序类型值是否是Set集合的成员”。从5.2.2节“预定义枚举类型”可知,能够编写一个条件测试以判断某个控件是否可以拥有其他控件。

if( csAcceptsControl in ControlStyle ) then

// perhaps assign the parent property of the control

对于判断对象的状态来说,in操作符是很有价值的,特别是控件在生命周期中进行转换时。由于in操作符需要左侧的有序类型的操作数,因此它不能用于判断是否多个元素是某个集合的成员。对于判断多个元素的成员资格问题,仍然需要集合代数。

测试集合的交集可用于判断是否两个集合中都包含两个或多个元素。给出两个集合[1, 2, 3]和ASet,都定义为单字节整数的集合;如果变量ASet包含子集[1, 2, 3],则交集语句结果为真。

if( [1, 2, 3] * ASet = [1, 2, 3] ) then

// True test code here

*(交集操作符)等价于逻辑与测试。+(并集操作符)等价于逻辑或测试。

5.3.4  Include和Exclude过程

如果觉得集合代数较为深奥的话,您可以使用System.pas单元中定义的Include和Exclude过程。Include和Exclude过程有两个参数,并在第一个参数中返回修改后的集合。过程定义如下。

procedure Include( var S : set of T; I : T );

procedure Exclude( var S: set of T; I : T );

把要添加或删除元素的集合以及对应的元素作为参数传递,过程即可返回结果集。Include过程等价于S := S + [I] 而Exclude等价于S := S – [I]。Delphi的帮助文档中指出,使用Include和Exclude过程与冗长的集合代数语句相比可以生成更为高效的代码。如果我们在前一节中使用TSet进行演示,则可以向TSet变量中添加或删除成员,如下所示:

var

ASet : TSet; // TSet = set of byte;

begin

ASet := [4, 5, 6]; // set construction

Include( ASet, 8 );

Exclude( ASet, 6 );

end;

在前一节中ASet定义为TSet类型的变量(回忆一下,TSet定义为单字节整数的集合)。ASet初始化为[4, 5, 6]。调用Include在ASet中添加了元素8,而调用Exclude在集合中删除了元素6。结果集为[4, 5, 8]。如果试着加入与集合的基类型不符的元素,则该操作会被忽略。对于删除也是如此,删除集合中不存在的成员,操作同样会忽略。

5.4  掌 握 数 组

数组存在于较早的Object Pascal代码中,在较新的代码中偶尔也会用到。可能的情况下,最好使用TList或TCollection。但您仍然会用到数组,它们对于新的代码还是很有用的。这里涉及了可能出现的与上下文相关的数组的变体。

5.4.1  数组异常

数组定义了索引的范围,限定了数组中可能的元素数目以及每个元素的索引值。因此Delphi中的数组可能有任何起始索引和结束索引,而不限制以0或1为基点。如果数组索引越界,就会发生异常。如果Project Options对话框中的Compiler属性页上的Range checking复选框被选中,如图5.4所示,则在运行时会引发ERangeError异常。如果没有选中Range checking设置,程序可能会引发EAccessViolation异常,也可能不出现异常。至少会引发访问违例,这样您就知道内存被重写了。

图5.4  当调试和测试程序时,请设置Project Options 对话框中Compiler属性

       页上的Range checking复选框。这对于编译后的程序可能增加一些额

     外的开销,但您可以在程序没有错误并准备发行时,取消这一选项

var

S : array[1..10] of strings;

I : integer;

begin

I := 11;

S[I] := 'Delphi 6 Developer's Guide';

S[11] := 'Written by Paul Kimmel';

end;

即使I是无效的索引,S[I]一行仍然可以编译,但可能在运行时引发异常。而直接使用索引值11的第二行则不能编译。无论是否选中边界检查选项,第二种类型的过界违例都会导致编译错误。

选中边界检查选项,编译器将向编译后的应用程序添加代码,因此在调试和测试时是个不错的选项。一旦已经根除了所有的边界错误,当发行应用程序时就可以取消边界检查选项。请记住,访问违例可能是内存泄漏或内存重写,但它比这两种错误害处更大。选中Project Options对话框中的Range checking复选框可以在整个程序中添加边界检查代码,而使用{$R}编译器指令则可以在相关编译器指令之间添加局部的边界检查代码(例子请参见5.3.2节“使用集合构造器”)。

5.4.2  定义子界值

子界可以直接定义,也可以作为命名类型定义。直接定义的子界语法形如n .. n+m,其中n和m为有序类型。这样数组就可以使用诸如字符、布尔值、枚举或整数等类型值进行索引。为使代码更加简练,通过使用一些技巧,您可以将类型定义为某个范围的值。例如您需要有10000个索引的数组,可以使用子界1..10000。将子界定义为命名类型,可以使代码可读性更好。

type

TIntegerRange = 1..10000;

var

Ints : array[TIntegerRange] of Integer;

注意:对数组大小的限制是2G字节。因此无法使用整数作为索引类型创建栈数组,因为索引值有2 147 000 000(超过20亿)个。即使以整数为索引的字符数组,都已经超过了2G字节的限制。

前面列出的声明使得可以使用整数值索引数组,而不必超出2G字节的数组大小限制。Delphi中的子界可以从任意下界到任意上界,只要上界值大于下界值即可。这样,既无须像Visual Basic一样指定数组基点选项为0或1,也不需要像C和C++一样受限于0基点的数组。

5.4.3  使用类型减少边界错误

为减少对数组进行索引时的越界错误,可以使用枚举类型、类型别名或新的类型以确保所有的索引都在可接受的范围之内。

type

TEnums = (Enum1, Enum3, Enum4);

TAlias = byte;

TChar = type Char;

提示:可以使用type newtype = type oldtype来引入新的、独立的类型,它由通用数据类型派生而来,但在问题域中可能比后者具有更直观的意义。强类型的Pascal编译器将对var和out参数强制实施类型兼容检查,但对参数类型较为陈旧的过程可允许进行强制类型转换。

TEnums定义了可用来索引数组的枚举类型。这种索引方式比简单的使用整数更加易于理解。TAlias是byte类型的别名。以TAlias为索引类型的数组与索引类型为byte的数组是等价的。第三个类型定义引入了一个新的类型TChar,它不是别名而是类型,编译器将对该类型进行强制性类型兼容检查。

在type语句中=的右侧使用type将引入新的类型(参见前面列出的代码)。因此,例子中的TChar与char是不同的类型。对需要新类型参数的过程,编译器将对类型进行强制性检查,当实际类型与var或out参数的类型不匹配时,则无法继续编译。过程ArrayRange示范了如何定义使用引入的类型作为索引值的数组。

Procedure ArrayRange;

type

TEnums = (Enum1, Enum3, Enum4);

TAlias = byte;

TChar = type Char;

var

E : array[TEnums] of string;

A : array[TAlias] of char;

C : array[TChar] of byte;

begin

E[Enum1] := 'An enumerated index.';

A[255] := #65;

C['C'] := 255;

ShowMessage( E[Enum1] );

ShowMessage( A[255] );

ShowMessage( IntToStr(C['C']) );

end;

当使用类型别名时,至少可以提高代码的可读性。如果使用引入的新类型,则编译器可以进行一些帮助。对于数组索引,请尽可能使用枚举、类型别名和新的类型,而不要使用内建类型。

5.4.4  下界与上界函数

可以使用Low和High两个函数,以确保使用数组时索引没有越界。Low函数有一个无类型参数,返回数组的下界。例如var S : array[5..7],当把S传递给Low时,结果为5。High返回数组的上界。

注意:Low和High函数对数组和有序类型总是可以正确工作,包括枚举类型在内。

可以使用Low和High函数来代替直接写出的常数值。这样就无须记忆数组的上下界,而且即使改变了数组的上下界代码仍然是正确的。在下面的代码实例中,分别示范了使用和不使用Low和High函数的数组索引用法。

Procedure IndexingExample;

type

TLimit = 1..100;

var

OldStyle : array[1..100] of Integer;

PreferableStyle : array[TLimit] of integer;

I : Integer;

J : TLimit;

begin

for I := 1 to 100 do

OldStyle[I] := I;

for J := Low(TLimit) to High(TLimit) do

PreferableStyle[J] := J;

end;

在上面的例子中,OldStyle数组的索引是直接定义的子界1..100。begin end块语句对索引进行了硬编码。如果改变了OldStyle的边界,代码就出错了。PreferableStyle数组使用了类型别名作为索引类型,并使用Low和High边界函数,这样即使改变了TLimit的范围,也可以双重的保证不会出现错误的索引值。

5.4.5  开放数组参数

定义过程时,对指定类型或可变类型的数组参数可以不指定其索引范围。开放参数字符数组的例子是C : array of char。元素的个数是未知的,每个元素都是字符。使用上一节的Low和High函数可以得到索引的上下界。可变类型的参数数组可以写作V : array of const。前者较为容易使用,因为基类型是已知的,只需要判断索引的范围。而后者,即可变类型的数组,则需要进行类型检查和边界检查。

类型化参数数组

在类型化数组参数的声明中,指明了数组元素的类型。例如,整数数组可以写作IntArray : array of Integer。下面的排序算法以整数数组为参数,对其元素进行了简单的冒泡排序。

提示:不能把常数传递给开放数组参数。

 

procedure BubbleSort( var IntArray : array of Integer );

var

I, J, Temp : integer;

begin

for I := Low(IntArray) to High(IntArray) - 1 do

for J := I + 1 to High(IntArray) do

if( IntArray[I] > IntArray[J] ) then

begin

Temp := IntArray[J];

IntArray[J] := IntArray[I];

IntArray[I] := Temp;

end;

end;

过程的定义示范了如何定义类型化参数数组(您可能已经知道冒泡排序算法)。使用有任意个元素的整数数组调用BubbleSort,都可以正确对数组进行排序。

我们将详细说明BubbleSort的这个简单实现,以便继续讨论类型化数组参数。考虑一下按降序对数组进行排序(上面的代码按升序对数组排序)。很显然,逆转条件测试用小于比较代替大于比较,就可以实现另一个版本的排序过程。这种技术是显然的,但并不高级。考虑到两种版本的排序中只有一行代码是不同的,那就可能只对那一行代码进行修改。考虑对排序过程的如下修订。

type

TCompareProc = Function(Elem1, Elem2 : Variant ) : Boolean;

 

Function GreaterThan( Elem1, Elem2 : variant) : Boolean;

begin

result := Elem1 > Elem2;

end;

 

Function LessThan(Elem1, Elem2 : Variant ) : Boolean;

begin

result := Elem1 < Elem2;

end;

 

procedure Swap( I, J : Integer; var IntArray : array of Integer);

var

Temp : Integer;

begin

Temp := IntArray[I];

IntArray[I] := IntArray[J];

IntArray[J] := Temp;

end;

 

procedure BubbleSort( var IntArray : array of Integer;

CompareProc : TCompareProc );

var

I, J : integer;

begin

for I := Low(IntArray) to High(IntArray) - 1 do

for J := I + 1 to High(IntArray) do

if( CompareProc( IntArray[I], IntArray[J] )) then

Swap( I, J, IntArray );

end;

类型声明部分定义了过程类型TCompareProc,该类型的变量值,可以是任何具有两个可变类型参数、返回布尔值的函数。紧接着的两个函数GreaterThan和LessThan,恰好与TCompareProc的原型相匹配。在诸如选择排序、快速排序和其他之类的排序算法中,需要进行元素交换,因此为使排序算法显得更加整洁,又进一步定义了Swap过程。修改后的BubbleSort过程增加了一个过程类型的参数。可以看到实现所需的代码更少。当把数组和进行特定类型比较的过程传递到BubbleSort算法时,即可按指定的顺序进行排序。使用修改后的代码,对BubbleSort的调用看起来可能形如BubbleSort( intarray, GreaterThan);或者BubbleSort(intarray, LessThan);,其中intarray表示整数数组,LessThan或GreaterThan是按照需要的次序进行排序的比较函数。

可以作进一步的修改,使得代码在尽可能灵活的同时保持其易用性,可将对BubbleSort的调用包装在过程中,所需的排序次序用名字来表示而无须传递过程类型的参数。或者可以为过程参数提供默认值,因而用户只需在通常情况——按升序排序无法满足需要的情况下,才需要传递比较函数作为过程参数。通过以累积的方式对这些技术进行合并,您可以生成简练、无冗余、非常灵活的代码,同时又只需少量的测试。

常量数组或可变类型数组

当参数以array of const形式声明时,其类型是动态的或在编译时未知。每个元素都是可变数据类型的。可变类型携带了许多信息。因此在增强灵活性的同时,代码大小也会增加。但有时候确实需要这样作。array of const与基类型为TVarRec的数组是等价的。TVarRec是紧缩形式的记录,与C或C++中的联合结构很相似,前者所有的数据都位于同一位置。其表现形式取决于所访问的记录成员。

procedure OpenArray( S : array of const );

var

I : Integer;

begin

for I := Low(S) to High(S) do

ShowMessage(S[I].VPChar) );

end;

 

OpenArray( ['This', 'is', String('a'), 'test'] );

上面的例子中OpenArray有一个参数S,定义为array of const(或TVarRec)。请记住数组中的每个元素类型都是TVarRec,因此可以访问TVarRec的某一特定成员来引用所包含的数据。TVarRec的VType成员可用于动态地确定数据的类型。如果类型在编译时已经知道,就可以像前面代码中调用ShowMessage一样,直接访问TVarRec中特定的数据元素(关于system.pas单元中的TVarRec类型,请参考帮助,即可知道它包含的所有可能的数据类型)。

在array of constant类型的数组中,可以使用TVarRec记录中的VType元素,通过if或case语句来动态确定数组中元素的类型。下面演示了如何使用case语句来这样做。

for I := Low(S) to High(S) do

case S[I].VType of

vtAnsiString : ShowMessage( S[I].VPChar );

vtChar : ShowMessage( S[I].VChar );

vtInteger : ShowMessage( IntToStr( S[I].VInteger ));

else

// do nothing!!

end;

这里的for语句对OpenArray过程中的for语句做了一些修改。数组中的每个元素都与三种可能的类型进行比较,并打印出对应的值。不能用TVarRec进行隐式类型转换。例如通过TVarRec的VPChar访问整数,将导致EAccessViolation异常。

5.4.6  定义静态数组和动态数组

静态数组是指在定义语句中指定了元素数目的数组。数组语句中的array[n..m+n]子句就表明了元素的数目以及数组是静态的。本章中,您可能已经看到过许多这种类型的数组。动态数组的定义与开放数组参数有些相似,元素的数目没有指定,是在运行时定义的。

设置动态数组的大小

动态数组的声明与静态数组大致相同。语法上最显著的不同是没有指定元素数目的[]。例如,Ints : array of integer定义了一个没有元素的动态数组变量。

当声明动态数组时,变量值为nil,因此if (Ints = Nil ) then条件测试结果总为True。动态数组开始时是没有元素的。将数组和所要的大小传递给SetLength过程,即可设置数组的大小,然后它就可以包含若干元素。

var

B : array of integer;

begin

if( B = Nil ) then ShowMessage( 'B = Nil' );

SetLength(B , 10);

if( B = Nil ) then ShowMessage( 'B = Nil' );

end;

在代码中,B是整数的动态数组。第一个条件测试结果B=Nil为真,因此显示了ShowMessage对话框。第二行代码动态地将数组的大小设置为10。动态数组总是使用整数进行索引,第一个索引为0,最后一个索引为n-1。这样,在上面的代码中,B的有效索引从0到9。当动态数组分配存储空间后,即可像静态数组那样使用。

注意:写作本章时,Beta 2版本的文档的声称短字符串的动态数组只能有0到255个元素。当进行测试时,发现编译器并未加入这样的限制。

不能对动态数组使用间接引用操作符^以及New和Dispose过程。如果这样做,将会出现编译时错误以提醒您。建议使用Copy方法来截断数组,尽管SetLength也可以工作并保留了数组中的元素。例如要把上述代码中的B截断到5个元素,Copy(B, 0, 5)即可去掉后面5个元素。

创建可变类型数组

可变类型数组可以通过VarArrayCreate来动态地分配。可以将VarArrayCreate的返回值赋予可变类型的变量。VarArrayCreate的参数包括表示数组边界的整数数组以及可变类型的编码。可变类型编码定义在system.pas中。

注意:在Delphi 5中VarArrayOf和VarArrayCreate定义在system.pas中。可以回想到,system.pas是自动包含的。在Delphi 6中,VarArrayOf和VarArrayCreate定义在Variants.pas中,因此在使用这两个函数前需要用Uses子句包括该单元。

var

V : Variant;

begin

V := VarArrayCreate( [0, 3], varVariant);

V[0] := 1;

V[1] := 'Test';

V[2] := VarArrayOf( [1, 'a', 1.0] );

end;

列出的代码示范了如何创建可变类型数组,并将其赋值给变量V。每个元素的类型定义为varVariant,即一种可变类型。当数组已经分配后,可以像其他数组一样对可索引的项进行赋值,如代码所示。代码的最后一行演示了创建并初始化可变类型数组的另一种方法,使用VarArrayOf函数。

如代码所示,VarArrayOf函数有一个参数,为可变类型数组。在VarArrayOf内部,它调用VarArrayCreate创建可变类型数组,并把参数的每个元素复制到创建的数组。您可能奇怪为什么不直接进行赋值V := [1, 2, 3]。部分的原因是因为这是集合构造器的形式,而集合类型出现在由COM所引入的可变类型之前(看一看variants.pas中的VarArrayCreate的代码,了解其复杂性后,有助于明白为什么要迂回地创建并初始化可变类型数组)。

5.4.7  紧缩数组

为进行更快速的访问,编译时数组中的元素对齐到字或双字边界。在定义语句中使用packed关键字,可以压缩数据但访问速度会有所降低。

var

U : array[1..100] of record R : Real; S : String; end;

P : packed array[1..100] of record R : Real; S : String; end;

列出的代码定义了记录数组U,每个元素包含实数R和字符串S,还定义了P,与U类型相同的紧缩数组。对U调用SizeOf,其大小为1600字节,而P的大小则为1200字节。在日常程序中并不需要紧缩数组,但如果需要它们确实是可用的。

5.5  运行时类型信息

typinfo.pas和system.pas单元中包含了一些用于处理运行时类型信息的过程。运行时类型信息是通过Pascal记录来实现的,它存储了一些变量的额外信息,包括类型和名字等。该信息使得可以对变量和对象进行查询,以判断其实际的数据类型。例如,TNotifyEvent传递了一个TObject对象到事件处理程序,但实际类型很少会是TObject。把TNotifyEvent的参数定义为TObject类型,使得可以把一个事件处理程序用于许多对象。可以用运行时类型信息来判断实际的对象类型。

TObject类定义在system.pas单元中,并定义了几个类方法,可以判断对象的名字、类型、大小、祖先和父类等。在表5.3中列出了这些方法。

表5.3  运行时类型信息(RTTI)方法,可帮助找到对象的大小、类型、祖先和名字

声明

描述

class function ClassName : ShortString;

类方法,从虚方法表中的表项返回类的名字

function ClassType: TClass;

返回对象的类

class function InheritsFrom(AClass : TClass):Boolean;

返回布尔值,表示参数类是否是调用对象的祖先

class function ClassParent: TClass;

返回类的直接祖先(并非其拥有者控件)

class function InstanceSize: Longint;

类方法,返回虚方法表的表项,表示该类型的对象需要分配多少内存

class function ClassInfo:Pointer;

返回指向TTypeInfo记录的指针(定义在TypInfo.pas中)

这些方法中的许多在VCL低层中被用到,但当您编写组件或应用程序时可能也会偶尔用到。

RTTI编译器指令{$M}与+联用时,表示在编译代码时加入RTTI信息。涉及到VCL时,RTTI信息是在TPersistent类中引入的,这样TPersistent类的每个派生类都包含了运行时类型信息。运行时动态类型识别在组件的事件处理程序中用得最为普遍,将在下一节讨论。

5.6  类 型 转 换

RTTI是Delphi运转的关键。可以想像,如果每个组件都需要自身的单击事件处理程序,那么会怎么样。每个组件都需要一个惟一的过程类型,而仅仅一个TNotifyEvent过程类型是不够的(回想一下,TNotifyEvent有一个参数,Sender : TObject)。实际上,每个事件处理程序都至少有一个类型为TObject的参数。如果没有RTTI,那么每个组件的每个事件处理程序都需要一个不同的过程类型,因此会使得VCL庞大而复杂。

RTTI使用is操作符进行运行时类型检查。例如像if (Sender is TButton) then 这样的代码,使得程序员可以判断某一特定类型的对象。如果对象是某一类型的,就可以把TObject类型的对象强制或动态转换到合适的类型。下面代码演示了类型的强制转换与动态转换。

if( Sender is TButton ) then

ShowMessage( TButton(Sender).Name ); // type.coercion

if( Sender is TButton ) then

ShowMessage( (Sender As TButton).Name ); //type.casting

第一个if条件语句强制地把Sender转换为TButton类型。第二个条件语句进行了动态类型转换。这两种形式都能够通过一般形式的Sender(TObject)参数访问TButton的数据和方法,但如果对象并非As操作符右侧的类型,第二种形式将引发EInvalidCast异常。下面的代码是窗体的单击事件的处理程序。当窗体被单击时,调用事件处理程序FormClick,其中Sender类型为TForm。

procedure TForm1.FormClick(Sender: TObject);

begin

ShowMessage( TButton(Sender).Name );

ShowMessage( (Sender As TButton).Name );

end;

虽然Sender并非TButton类型,但第一行代码居然奇迹般的可以工作。而第二行代码会引发EInvalidCast异常。如果利用假造的强制类型转换调用不存在的方法或访问不存在的数据,其行为是未定义的。结果可能是令人不愉快的访问违例或内存重写。

5.7  小  结

第5章示范了枚举、常数、数组和运行时类型信息等技术。通过使用这些技术,代码的意图可以表达得更清楚,减少边界检查代码,使代码更加健壮并具有动态性。本章中的这些术语引入的过程,也就是编写编译器的工程师逐渐了解哪些东西会使程序崩溃的过程。如果编译器知道的代码信息越多、代码使用受到的约束越多,那么代码的可靠性就越强。

 

相关阅读 更多 +
排行榜 更多 +
谷歌卫星地图免费版下载

谷歌卫星地图免费版下载

生活实用 下载
谷歌卫星地图免费版下载

谷歌卫星地图免费版下载

生活实用 下载
kingsofpool官方正版下载

kingsofpool官方正版下载

赛车竞速 下载