处 理 异 常
解决错误是每个程序人员必有的经历,编写安全、健壮的应用程序是每个程序员的始终追求。然而,产生错误总是难免的,尽管程序员尽职尽责,即使程序没有一个错误,各种与程序一起工作的软件、硬件也可能发生错误。一个可靠的程序应该可以处理所有可能导致程序停止或产生不恰当结果的状态。
这种状态在Delphi中称之为异常(Exception)。Delphi提供了高级异常处理机制,可以确保程序从可能发生的错误中恢复过来,而且可以保证数据或资源不丢失,并在需要的情况下,关闭系统。
每个Delphi程序都有一个缺省的异常处理器,用来显示错误消息,防止程序意外停止。通过加入附加异常处理,用户可以更好地处理应用程序对意外条件的响应,以便当麻烦发生时,执行清理工作,保存重要数据和资源。下面,我们就来讨论有关Delphi异常处理机制的使用。
9.1 异常的产生
设计健壮程序的关键之一是,如果程序分配了资源,最后就必须释放它,而不管异常是否发生。例如,如果你的程序分配了一块内存,必须确保最终释放这块内存;如果打开了一个文件,必须确保最后关闭这个文件。
在正常情况下,程序在申请资源后,能够释放它。然而,就算异常发生了,也必须保证释放它。
异常往往由一些偶然的事件导致。例如,程序中调用一个RTL例程,或使用一个构件都可能产生异常。你的程序必须确保即使这些偶然事件发生了,也要释放被占用的资源。
常见的需要保护的资源有:文件、内存、Windows资源 、各种对象等。
9.2 异常处理语句
要处理异常,首先必须识别异常,判断异常在什么时候发生,然后对出现的异常作出处理。因此对于程序员来说,其工作就是确定程序中哪个地方产生错误及发生错误时的处理程序,特别是容易导致数据或系统资源丢失的错误。
被保护代码块是用户处理异常的基本工具。对异常的响应是在代码保护块中进行的。当有若干条语句需要处理同一个错误时,可以将这些语句放在一个代码块里,并对整个代码块定义一个响应过程。这个对异常响应的代码块称为保护块。
处理代码保护块异常及其异常处理有两种方式,即try...finally语句和try...except语句。这两种语句可以互相嵌套,形成更加牢固的异常处理结构。
9.2.1 try...finally
有时侯,我们只需要程序中操作某项任务的代码执行完成,而不管是否发生了异常。例如,当一段程序获得了某项资源时,我们需要释放它,而不管程序是否正常终止。在这种情况下,就可以使用try...finally语句。
try...finally语句的语法结构是:
try statementList1 finally statementList2 end
这里statementList1和 statementList2是由分号隔开的一系列执行语句。程序首先从statementList1开始执行,如果一切正常,将依此执行statementList2中的语句。但是,当程序在statementList1执行时产生异常时,程序将直接从出错的地方跳到statementList2执行。因此,statementList2语句总是会被执行的,无论statementList1程序是否出错。statementList2语句通常用于释放程序占用的资源。
又例如,下面是一段操作文件的代码:
Reset(F);
try
... // process file F
finally
CloseFile(F);
end;
文件用来存放在硬盘的资源,在打开后,不管操作情况如何,最后都必须关闭它,否则容易造成数据丢失。
但是,假如statementList2语句中出现了问题,并且触发了异常,则该异常就被传递到try...finally语句的外面,同时原来在statementList1语句中触发的所有异常都将丢失。因此,try...finally语句也有一定缺陷。
9.2.2 try... except
异常通常是在try... except中进行的。
try...except 语句的语法结构是:
try statements except exceptionBlock end
这里statements是由分号隔开的一系列执行语句。exceptionBlock可以是由分号隔开的一系列执行语句,也可以是一系列专门处理异常的程序句柄。
处理异常的程序句柄的语法规则是:
on identifier: type do statement
这里,identifier是任何合法的标识符,也可以省略,type表示异常的类型,statement是执行语句,但如果是复合语句,必须用begin...end括起来。处理异常程序句柄之后可以跟有else statements语句。
try...except 语句首先从statements开始执行,如果一切正常,exceptionBlock将被忽略,直接执行try...except 语句后面的语句。但是,当程序statements在执行中产生异常时,程序将直接从出错的地方跳到exceptionBlock执行。
exceptionBlock执行时,首先按顺序判断处理块中异常程序句柄中异常的类型,如果有一个与产生的异常类型相同,则执行类型后面的statement语句,如果没有与产生的异常类型相同的类型,但是在exceptionBlock后面有else子句,则程序转到该else子句执行。如果exceptionBlock仅仅是由一些普通的语句组成,则程序转到其中的第一条语句执行。如果在exceptionBlock什么也没有,则该异常将向外传递。
下面是一个例子:
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
end;
在上面的代码中,第一个异常处理句柄处理被0除异常;第二个异常处理句柄处理溢出异常;第三个异常处理句柄处理数学运算错误异常。这里,第三个异常处理句柄放在最后,是因为数学运算错误异常包含了前面两个异常类,如果它放在前面,则其它异常处理都不会被执行。前面我们看到,异常类名的前面可以加一个标识符。这个标识符用来在该异常句柄中表示该异常对象。这个标识符的使用范围只限于该异常句柄。例如:
try
...
except on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;
如果异常块后面带有else子句,则else子句用于处理没有被exceptionBlock处理的任何异常。
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
else
HandleAllOthers;
end;
这里,else子句处理任何不是EMathError异常类的异常。如果exceptionBlock仅仅是由一些普通的语句组成,则产生的任何异常都会执行这些语句。例如:
try
...
except
HandleException;
end;
这里,HandleException例程将处理执行try和except之间语句时发生的任何异常。
9.2.3 嵌套异常响应
由于Pascal允许语句嵌套,因此,可以在已经定义了响应的保护块内再定义响应,即try...finally语句和try...except语句可以互相嵌套。
9.3 缺省的异常处理
如果你没有对异常作出响应,Delphi就提供缺省的处理方式。Delphi缺省异常处理会提示一个错误信息。Delphi缺省异常处理实际上是调用TApplication类的HandleException方法。
对于所有异常(EAbort除外), HandleException首先检查OnException事件句柄是否存在。如果存在,则执行这个事件句柄。如果不存在,就自动调用SHowException方法。
如果你既要处理异常,又要像VCL构件那样提供缺省的行为,可以在响应异常时,调用HandleException方法。例如下面代码
try
{ 保护语句 }
except
on ESomething do
begin
{ 处理异常 }
Application.HandleException(Self); {调用HandleException }
end;
end;
另外,OnException事件的处理代码可以被修改,可以用自己的代码代替缺省的异常处理句柄。办法就是响应OnException事件,参考下面的程序示例。这段程序显示,如果异常没有被处理,则调用缺省异常处理。缺省异常处理显示错误,结束程序。procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException := AppException;
end;
procedure TForm1.AppException(Sender: TObject; E: Exception);
begin
Application.ShowException(E);
Application.Terminate;
end;
9.4 Exception类
Exception类是所有异常类的基类,其本身直接继承于TObject类,在Delphi的Sysutils单元中,Exception类的定义如下(只列出公共成员):
Type Exception=Class(TObject)
public
Constructor Create(const Msg:string);
Constructor CreateFmt(const Msg:string;const Args:array of const);
Constructor CreateRes(Ident:Integer);
Constructor CreateResFmt(Ident:Integer;const Args:array of const);
Constructor CreateHelp(const Msg:string;HelpContext:Integer);
Constructor CreateFmtHelp(const Msg:string;const Args:array of const;HelpContext:Integer);
Constructor CreateResHelp(Ident,HelpContext:Integer);
Constructor CreateResFmtHelp(Ident:Integer;const Args:array of const;HelpContext:Integer);
Property HelpContext:Integer;
Property Message:String;
end;
Exception类中只申明了两个特性:一个是Message特性,用于给出有关异常的简短说明;另一个是HelpContext特性,用于指定帮助的上下文编号,任何一个异常都可以访问这两个特性 。
Exception类提供了一些构造异常的方法。所有构造函数的名字都以Create开头,它们都有相同的目的,就是根据不同的参数来创建一个错误提示信息字符串。用户可以调用这些构造函数来创建自己的派生类的异常实例,或者可以为Exception对象调用它们。
例如,把一个字符串或变量作为错误消息来传递,生成异常的语句可以是:
raise Exception.Create('Error In Memory');
又例如,下面语句生成的异常显示带有两个整数的错误信息字符串:
raise Exception.CreateFmt('Error:X=%d Y=%d',{X,Y]);
下面是这个语句的一个运行结果: Error:X=-1 Y=12
9.5 自定义异常
当用户需要产生异常时,可以选择从Exception中派生出来的一个类或自己创建异常类。用户可以从Exception中派生自己的类,也可以设计一个全新的类,这样的类不需要建立在Exception的基础上。但是,Delphi建议定义异常时,最好用Exception类作为基类,因为不是从Exception中派生的类必须处理所有不是基于Exception的异常,任何未处理的异常都会引起致命的应用程序错误。
自定义异常类的程序如下:
type EMyException = class(Exception)
上面,定义了一个异常类,该类的基类是Exception.
当然,异常不一定必须直接从Exception类继承,也可以用其它Exception类的派生类作为自己的基类,例如:
type
EMathError = class(Exception);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
在声明异常时,可以根据需要声明一些字段、方法和特性。例如,SysUtils单元中的EInOutError异常就声明了一个ErrorCode字段,用于存储引起异常的文件I/O错误代码,示例如下:
type EInOutError = class(Exception)
ErrorCode: Integer;
end;
声明了自己的异常后,需要在程序中触发这个异常,使用Raise语句。Raise语句的语法格式:
raise object at address
这里object和 at address都是可选的。如果省略了object,表示要重新触发当前异常。 这种形式只能出现在try...Except结构的Except部分,主要用于过程或函数的调用中出现无法完全处理异常情况。raise后面的object是异常的对象实例,而不是异常的类型。通常把创建对象实例与触发这个异常并在一句中,因为构造函数返回的总是该类型的对象实例。如
EPasswordInvalid = class(Exception);
在触发异常时,创建对象实例与触发这个异常并在一句中。
if Password <> CorrectPassword then
raise EPasswordInvalid.Create('Incorrect password entered');
注意:虽然程序创建了异常对象实例,但程序不必自己删除它,因为异常处理句柄会自动删除异常的实例。
关键步骤是程序中定义了一个TMouseException类。其定义方法是:
type
TMouseException = class(Exception)
X, Y: Integer;
constructor Create(const Msg: string; XX, YY: Integer);
end;
TMouseException类是从Exception类派生出来的。除了从Exception继承来的成员外,TMouseException类还增加了整型成员X和Y,并说明了一个构件函数,可以调用这个构造函数来初始化该类的对象。其构造函数的编程为:
constructor TMouseException.Create(const Msg: string; XX, YY: Integer);
begin
X := XX; // Save X and Y values in object
Y := YY;
Message := // Create message string Msg + ' (X=' + IntToStr(X) + ', Y=' + IntToStr(Y) + ')';
end;
这里,Message是从Exception来继承过来的,它被赋值给了包括这两个值的一个消息。
Width := 65;{ Change inherited properties }
Height := 65;
FPen := TPen.Create;{ Initialize new fields }
FPen.OnChange := PenChanged;
FBrush := TBrush.Create;
FBrush.OnChange := BrushChanged;
end;
构造的第一行是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为0(对于有序类型)、nil(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了Inherited Create(Owner)这句外,不需要写任何代码。
如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个try協inally语句,例如:
try
...{ User defined actions }
except{ On any exception }
Destroy;{ Destroy unfinished object }
raise;{ Re-raise exception }
end;
构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。
2.析构
析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下:
destructor TShape.Destroy;
begin
FBrush.Free;
FPen.Free;
inherited Destroy;
end;
上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。
析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。
前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为Nil。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush.Free和FPen.Free,Free方法的实现是:
procedure TObject.Free;
begin
if Self <> nil then Destroy;
end;
也即Free方法在调用Destroy前会自动判断指针是否为Nil。如果改用FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产生异常导致程序中止。
7.2.3 方法指令字
声明方法的语法规则中,method directives为方法的指令字。
从语法示意图中可以看出,方法按指令字分又可分为三种,分别是虚拟、动态、消息方法,它们分别是方法名后用Virtual,Dynamic,Message保留字指定。也可以不加方法指令字,这种情况下声明的方法是静态的(static)。
另外,从语法示意图中可以看出,一个方法也可以像函数那样,指定参数的传递的方式,也即方法的调用约定。一个方法调用约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。
1.静态方法
缺省情况,所有的方法都是静态的,除非你为方法提供了其它指令字。静态方法类似于通常的过程和函数,编译器在编译时就已指定了输出该方法的对象实例。静态方法的主要优点是调用的速度快。
当从一个类派生一个类时,静态方法不会改变。如果你定义一个包含静态方法的类,然后派生一个新类,则被派生的类在同一地址共享基类的静态方法,也即你不能重载静态方法。如果你在派生类定义一个与祖先类相同名的静态方法,派生类的静态方法只是替换祖先类的静态方法。例如:
type TFirstComponent = class(TComponent)
procedure Move; procedure Flash;
end;
TSecondComponent = class(TFirstComponent)
procedure Move;{该MOVE不同于祖先类的MOVE}
function Flash(HowOften: Integer): Integer;{该Flash不同于祖先类的Flash}
end;
上面代码中,第一个类定义了两个静态方法,第二个类定义了于祖先类同名的两个静态方法,第二个类的两个静态方法将替换第一个类的两个静态方法。
2.虚拟方法
虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定的,而是程序在运行期根据调用这个虚拟方法的对象实例来决定的,这种方法又为滞后联编。 虚拟方法在对象虚拟方法表(VMT表)中占有一个索引号。
VMT表保存类类型的所有虚拟方法的地址。当你从一个类派生一个新类时,派生类创建它自己的VMT,该VMT包括了祖先类的VMT,同时加上自己定义的虚拟方法的地址虚拟方法可以在派生类中重新被定义,但祖先类中仍然可以被调用。例如:
type TFirstComponent = class(TCustomControl)
procedure Move;{ static method }
procedure Flash; virtual;{ virtual method }
procedure Beep; dynamic;{ dynamic virtual method }
end;
TSecondComponent = class(TFirstComponent)
procedure Move;{ declares new method }
procedure Flash;
override;{ overrides inherited method }
procedure Beep; override;{ overrides inherited method }
end
;
上例中,祖先类TFirstComponentw中方法Flash声明为虚拟的,派生类TSecondComponent重载了方法Flash。声明派生类的Flash 时,后面加了一个Override指令字,表示被声明的方法是重载基类中的同名的虚拟或动态方法。
注意:重载的方法必须与祖先类中被继承的方法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型一致。
要重载祖先类中的方法,必须使用Override批示字,如果不加这个指令字,而在派生类中声明了于祖先类同名的方法,则新声明的方法将隐藏被继承的方法。
3.动态方法
所谓动态方法,非常类似于虚拟方法,当把一个基类中的某个方法声明为动态方法时,派生类可以重载它,如上例的Beep。不同的是,被声明为动态的方法不是放在类的虚拟方法表中,而是由编译器给它一个索引号(一般不直接用到这个索引),当调用动态方法时,由索引号决定调用方法的哪个来具体实现。
从功能上讲,虚拟方法和动态方法几乎完全相同,只不过虚拟方法在调用速度上较快,但类型对象占用空间大,而动态方法在调用速度上稍慢而对象占用空间小。如果一个方法经常需要调用,或该方法的执行时间要求短,则在虚拟和动态之间还是选择使用虚拟为好。
4.消息句柄方法
在方法定义时加上一个message指令字,就可以定义一个消息句柄方法。消息句柄方法主要用于响应并处理某个特定的事件。
把一个方法声明为消息句柄的示例如下:
type
TTextBox = class(TCustomControl)
private
procedure WMChar(var Message: TWMChar); message WM_CHAR;
...
end;
上例中声明了一个名叫TTextBox的类类型,其中还声明了一个过程WMPaint,只有一个变量参数Message,过程的首部后用保留字Message表示这是个消息句柄,后跟一个常量WM_PAINT表示消息句柄要响应的事件。
Object Pascal规定消息句柄方法必须是一个过程,并且带有一个唯一的变量参数。message保留字后必须跟随一个范围在1到49151的整型常量,以指定消息的ID号。注意,当为一个VCL控制定义一个消息句柄方法时,整型常量必须是Windows的消息ID。(Delphi的Messages单元列出了所有Windows的消息ID。
注意:消息句柄不能使用Cdecl调用约定,也不能用Virtual,Dynamic,Override或Abstract等指令字。
在消息句柄中,你还可以调用缺省的消息句柄,例如上例中,你声明了一个处理WM_PAINT消息的方法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的方法名称不一样,也就是说你未必知道缺省句柄,那怎么调用呢?没关系,Object Pascal只要你使用一个保留字Inherited就可以了,例如:
procedure TTextBox.WMChar(var Message: TWMChar); message WM_CHAR;
begin
Inherited
...
end;
上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用Inherited保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。
使用Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。
7.2.4 抽象方法
从图7.7的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。
抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。
声明一个抽象方法是用Abstract指令字,例如:
type
TFigure = class
procedure Draw; virtual; abstract;
...
end;
上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用Inherited保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。
7.2.5 重载方法与重定义方法
在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。
规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。
为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如:
type
AClass=Class
procedure One;virtual;
procedure Two;{static method}
end;
BClass=Clas(Aclass)
procedure One;override;
procedure Two;
end;
重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字inherited(继承)调用祖先类中相同的方法来实现。例如:
procedure Bclass.One;
begin //new code
...?
//call inherited procedure Bclass
inherited One;
end
;
在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如:
constructor Create(AOwner: TComponent); overload; override;
constructor Create(AOwner: TComponent; Text: string); overload;
如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如:
type
T1 = class(TObject)
procedure Test(I: Integer); overload; virtual;
end;
T2 = class(T1)
procedure Test(S: string); reintroduce; overload;
end;
...
SomeObject := T2.Create;
SomeObject.Test('Hello!'); // calls T2.Test
SomeObject.Test(7); // calls T1.Test
在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如:
type
TSomeClass = class
published
function Func(P: Integer): Integer;
function Func(P: Boolean): Integer // error
...
7.3 类 的 特 性
特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。
Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。
7.3.1 声明特性
要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。Object Pascal使用保留字Property声明特性。
特性的声明由保留字Property,特性标识符,可选的特性接口(Property Interface)和特性限定符(Property Specifier)构成。
特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。
在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和Write指定的方法去读或写字段的值。程序示例如下:
type TYourComponent = class(TComponent)
private
FCount: Integer; { used for internal storage }
procedure SetCount (Value: Integer); { write method }
public
property Count: Integer read FCount write SetCount;
end;
上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是Integer,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。
特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个Integer型变量AInteger,程序可以这么写:
AInteger:=Aobject.Count;
Aobject.Count:=5;
实际上,编译器根据声明中的Read子句和Write子句自动把上述语句分别转换成:
Ainteger:=Aobject.Fcount;
Aobject.SetCount(5);
顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用With语句则可简化。
跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。
7.3.2 特性限定符
特性限定符可以有四类,分别是Read,Write,Stored和Default。其中Read和Write限定符用于指定访问特性的方法或字段。
注意:Read和Write限定符指定的方法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。
1.Read限定符
Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。
从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。
2.Write限定符
Write限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。
在Write限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。
从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。
3.Stored限定符
Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。
Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为False时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为True,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。
含有Stored限定符的特性声明示例如下:
TSampleComponent = class(TComponent)
protected
function StoreIt: Boolean;
public { normally not stored }
property Important: Integer stored True;{ always stored }
published { normally stored always }
property Unimportant: Integer stored False;{ never stored }
property Sometimes: Integer stored StoreIt;{ storage depends on function value }
end;
上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。
4.Default和NoDefult限定符
Default限定符用于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过Default限定符设定的,程序示例如下 :
TStatusBar = class(TPanel)
public
constructor Create(AOwner: TComponent); override; { override to set new default }
published
property Align default alBottom; { redeclare with new default value }
end;
...
constructor TStatusBar.Create(AOwner: TComponent);
begin
inherited Create(AOwner); { perform inherited initialization }
Align := alBottom; { assign new default value for Align }
end;
上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。
注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。
如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。
7.3.3 数组特性
所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下:
type
TDemoComponent = class(TComponent)
private
function GetNumberName(Index: Integer): string;
public
property NumberName[Index: Integer]: string read GetNumberName;
end;
...
function TDemoComponent.GetNumberName(Index: Integer): string;
begin
Result := 'Unknown';
case Index of
-MaxInt..-1: Result := 'Negative';
0 : Result := 'Zero';
1..100 : Result := 'Small';
101..MaxInt: Result := 'Large';
end;
end;
上例中,声明了一个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。
对于数组特性来说,可以使用Read和Write限定符,但Read和Write限定符只能指定方法而不能是字段,并且Object Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。Write限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。
访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。
7.3.4 特性重载
所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。
最简单的重载,就是在派生类中这么写:
Property 特性名:
这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。
特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例:
type
TBase = class
...
protected
property Size: Integer read FSize;
property Text: string read GetText write SetText;
property Color: TColor read FColor write SetColor stored False;
...
end;
type TDerived = class(TBase)
...
protected
property Size write SetSize; published property Text;
property Color stored True default clBlue;
...
end;
对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,首先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了一个Default限定符。
7.4 类成员的可见性
面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么几个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。如
TBASE = class
private
FMinValue: Longint;
FMaxValue: Longint;
procedure SetMinValue(Value: Longint);
procedure SetMaxValue(Value: Longint);
function GetPercentDone: Longint;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
procedure AddProgress(Value: Longint);
property PercentDone: Longint read GetPercentDone;
published
property MinValue: Longint read FMinValue write SetMinValue default 0;
property MaxValue: Longint read FMaxValue write SetMaxValue default 100; property Progress: Longint read FCurValue write SetProgress
end;
上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表示它们是私有的,Public部分声明的几个方法是公共的。
再请看下面的例子:
TBASE = class
FMinValue: Longint;
FMaxValue: Longint;
private
procedure SetMinValue(Value: Longint);
procedure SetMaxValue(Value: Longint);
function GetPercentDone: Longint;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
procedure AddProgress(Value: Longint);
property PercentDone: Longint read GetPercentDone;
published
property MinValue: Longint read FMinValue write SetMinValue default 0;
property MaxValue: Longint read FMaxValue write SetMaxValue default 100;
property Progress: Longint read FCurValue write SetProgress;
end;
上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是用{$M+}状态编译的基类,其可见性为为Published,否则就是Public。
7.4.1 Private
在Private部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,Private部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。
7.4.2 Public
在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的Public部分声明,然后把Unit1加到Init2的Interface部分就可以了。
注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的Public部分,以防止程序意外地不正确地修改了数据。
7.4.3 Published
在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,Published和Public的区别在于成员的运行期类型信息不同。一个Published元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有Published接口,该接口被一些Delphi工具使用,例如Object Inspector。
注意:只有当编译开关$N的状态为$M+时或者基类是用$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M用于控制运行期类型信息的生成。
7.4.4 Protected
Protected与Private有些类似。在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。
在Protected部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。
7.4.5 Automated
C++的程序员可能对这个保留字比较陌生,在Automated部分声明的成员类似于在Public部分声明的成员,它们都是公共的,唯一的区别在于在Automated部分声明的方法和特性将生成OLE自动化操作的类型信息。
注意:Automated只适用于基类是TAuto0bject的类声明中,在Automated部分声明的方法,其参数和返回类型(如果是函数的话)必须是可自动操作的。在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可自动操作的,否则将导致错误。可自动操作的类型包括:Byte、Currency、Double、Integer、Single 、SmallInt、String、TDateTime、Variant、WordBool等。
在Automated部分声明的方法只能采用Register调用约定,方法可以是虚拟的但不能是动态的。在Automated部分声明的特性只能带Read和Write限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和Write指定的只能是方法而不能是字段,方法也只能采用Register调用约定,也不允许对特性重载。
在Automated部分声明的方法或特性分配一个识别号(ID),如果不带DispId限定符,编译器自动给方法或特性分配一个相异的Id,如果带DispId限定符,注意Id不能重复。
7.5 类类型的兼容性
一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码:
type
TScreenThing = Class
X,Y:Longint;
procedure Draw;
end;
T3DScreenThing = Class(TScreenThing)
Z:Longint;
end;
procedure ResetScreenThing(T:TScreenThing);
begin
T. X:=0;
U. Y:=0;
V. Draw;
end;
procedure Reset3DScreenThing(T:T3DScreenThing);
begin
T. X:=0;
T.Y:=0;
T.Z:=0;
U.Draw;
end;
var
Q:TScreenThing; R:T3DScreenThin;
begin
{...}
ResetScreenThing(Q);
ResetScreenThing(R); {this work}
Reset3DScreenThing(Q); { but this does not}
在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。
7.6 VCL类结构
我们介绍过的Delphi的VCL构件都是使用类类型定义的对象。在Delphi中,所有的类都是从一个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了一些操纵类的最基本的方法,是Delphi所有类的缺省祖先类。使用View|Browse命令,可以打开Browse Object命令,查看Delphi各对象之间的继承关系。
TObject类是一切构件类和对象的基类,位于继承关系的最顶层。TPersistent类是TObject类的下一级继承者,它是一个抽象类,主要为它的继承者提供对流的读写能力。
TComponent类是TPersistent类的下一级继承者,它是VCL中所有构件的祖先类。TComponent类定义了构件最基本的特性、方法和事件。尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有几个非可视的构件,如TTime构件和TDataSource构件等,绝大多数构件是从TComponent类的下级TControl类继承下来的,从TControl类继承下来的都是可视化的构件,这些构件也称为控制。TControl类定义了VCL中所有可视化构件基本的特性、方法和事件等。
TWinControl和TGraphicControl类都是TControl类的子类。TWinControl的子类主要是用于窗口控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗口,有自己的句柄,占用Windows资源,并且可以与用户交互。而TGraphicControl的子类没有窗口句柄,也不占用Windows资源类,也能接受键盘的输入,它们的主要优点在于节约资源,如TLabel和TSpeedButton等构件。
private
procedure WMChar(var Message: TWMChar); message WM_CHAR;
...
end;
上例中声明了一个名叫TTextBox的类类型,其中还声明了一个过程WMPaint,只有一个变量参数Message,过程的首部后用保留字Message表示这是个消息句柄,后跟一个常量WM_PAINT表示消息句柄要响应的事件。
Object Pascal规定消息句柄方法必须是一个过程,并且带有一个唯一的变量参数。message保留字后必须跟随一个范围在1到49151的整型常量,以指定消息的ID号。注意,当为一个VCL控制定义一个消息句柄方法时,整型常量必须是Windows的消息ID。(Delphi的Messages单元列出了所有Windows的消息ID。
注意:消息句柄不能使用Cdecl调用约定,也不能用Virtual,Dynamic,Override或Abstract等指令字。
在消息句柄中,你还可以调用缺省的消息句柄,例如上例中,你声明了一个处理WM_PAINT消息的方法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的方法名称不一样,也就是说你未必知道缺省句柄,那怎么调用呢?没关系,Object Pascal只要你使用一个保留字Inherited就可以了,例如:
procedure TTextBox.WMChar(var Message: TWMChar); message WM_CHAR;
begin
Inherited
...
end;
上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用Inherited保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。
使用Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。
7.2.4 抽象方法
从图7.7的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。
抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。
声明一个抽象方法是用Abstract指令字,例如:
type
TFigure = class
procedure Draw; virtual; abstract;
...
end;
上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用Inherited保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。
7.2.5 重载方法与重定义方法
在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。
规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。
为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如:
type
AClass=Class
procedure One;virtual;
procedure Two;{static method}
end;
BClass=Clas(Aclass)
procedure One;override;
procedure Two;
end;
重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字inherited(继承)调用祖先类中相同的方法来实现。例如:
procedure Bclass.One;
begin //new code
...?
//call inherited procedure Bclass
inherited One;
end;
在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如:
constructor Create(AOwner: TComponent); overload; override;
constructor Create(AOwner: TComponent; Text: string); overload;
如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如:
type
T1 = class(TObject)
procedure Test(I: Integer); overload; virtual;
end;
T2 = class(T1)
procedure Test(S: string); reintroduce; overload;
end;
...
SomeObject := T2.Create;
SomeObject.Test('Hello!'); // calls T2.Test
SomeObject.Test(7); // calls T1.Test
在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如:
type
TSomeClass = class
published
function Func(P: Integer): Integer;
function Func(P: Boolean): Integer // error
...
7.3 类 的 特 性
特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。
Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。
7.3.1 声明特性
要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。Object Pascal使用保留字Property声明特性。
特性的声明由保留字Property,特性标识符,可选的特性接口(Property Interface)和特性限定符(Property Specifier)构成。
特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。
在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和Write指定的方法去读或写字段的值。程序示例如下:
type TYourComponent = class(TComponent)
private
FCount: Integer; { used for internal storage }
procedure SetCount (Value: Integer); { write method }
public
property Count: Integer read FCount write SetCount;
end;
上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是Integer,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。
特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个Integer型变量AInteger,程序可以这么写:
AInteger:=Aobject.Count;
Aobject.Count:=5;
实际上,编译器根据声明中的Read子句和Write子句自动把上述语句分别转换成:
Ainteger:=Aobject.Fcount;
Aobject.SetCount(5);
顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用With语句则可简化。
跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。
7.3.2 特性限定符
特性限定符可以有四类,分别是Read,Write,Stored和Default。其中Read和Write限定符用于指定访问特性的方法或字段。
注意:Read和Write限定符指定的方法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。
1.Read限定符
Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。
从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。
2.Write限定符
Write限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。
在Write限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。
从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。
3.Stored限定符
Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。
Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为False时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为True,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。
含有Stored限定符的特性声明示例如下:
TSampleComponent = class(TComponent)
protected
function StoreIt: Boolean;
public { normally not stored }
property Important: Integer stored True;{ always stored }
published { normally stored always }
property Unimportant: Integer stored False;{ never stored }
property Sometimes: Integer stored StoreIt;{ storage depends on function value }
end;
上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。
4.Default和NoDefult限定符
Default限定符用于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过Default限定符设定的,程序示例如下 :
TStatusBar = class(TPanel)
public
constructor Create(AOwner: TComponent); override; { override to set new default }
published
property Align default alBottom; { redeclare with new default value }
end;
...
constructor TStatusBar.Create(AOwner: TComponent);
begin
inherited Create(AOwner); { perform inherited initialization }
Align := alBottom; { assign new default value for Align }
end;
上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。
注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。
如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。
7.3.3 数组特性
所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下:
type
TDemoComponent = class(TComponent)
private
function GetNumberName(Index: Integer): string;
public
property NumberName[Index: Integer]: string read GetNumberName;
end;
...
function TDemoComponent.GetNumberName(Index: Integer): string;
begin
Result := 'Unknown';
case Index of
-MaxInt..-1: Result := 'Negative';
0 : Result := 'Zero';
1..100 : Result := 'Small';
101..MaxInt: Result := 'Large';
end;
end;
上例中,声明了一个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。
对于数组特性来说,可以使用Read和Write限定符,但Read和Write限定符只能指定方法而不能是字段,并且Object Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。Write限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。
访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。
7.3.4 特性重载
所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。
最简单的重载,就是在派生类中这么写:
Property 特性名:
这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。
特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例:
type
TBase = class
...
protected
property Size: Integer read FSize;
property Text: string read GetText write SetText;
property Color: TColor read FColor write SetColor stored False;
...
end;
type TDerived = class(TBase)
...
protected
property Size write SetSize; published property Text;
property Color stored True default clBlue;
...
end;
对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,首先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了一个Default限定符。
7.4 类成员的可见性
面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么几个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。如
TBASE = class
private
FMinValue: Longint;
FMaxValue: Longint;
procedure SetMinValue(Value: Longint);
procedure SetMaxValue(Value: Longint);
function GetPercentDone: Longint;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
procedure AddProgress(Value: Longint);
property PercentDone: Longint read GetPercentDone;
published
property MinValue: Longint read FMinValue write SetMinValue default 0;
property MaxValue: Longint read FMaxValue write SetMaxValue default 100; property Progress: Longint read FCurValue write SetProgress
end;
上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表示它们是私有的,Public部分声明的几个方法是公共的。
再请看下面的例子:
TBASE = class
FMinValue: Longint;
FMaxValue: Longint;
private
procedure SetMinValue(Value: Longint);
procedure SetMaxValue(Value: Longint);
function GetPercentDone: Longint;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
procedure AddProgress(Value: Longint);
property PercentDone: Longint read GetPercentDone;
published
property MinValue: Longint read FMinValue write SetMinValue default 0;
property MaxValue: Longint read FMaxValue write SetMaxValue default 100;
property Progress: Longint read FCurValue write SetProgress;
end;
上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是用{$M+}状态编译的基类,其可见性为为Published,否则就是Public。
7.4.1 Private
在Private部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,Private部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。
7.4.2 Public
在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的Public部分声明,然后把Unit1加到Init2的Interface部分就可以了。
注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的Public部分,以防止程序意外地不正确地修改了数据。
7.4.3 Published
在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,Published和Public的区别在于成员的运行期类型信息不同。一个Published元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有Published接口,该接口被一些Delphi工具使用,例如Object Inspector。
注意:只有当编译开关$N的状态为$M+时或者基类是用$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M用于控制运行期类型信息的生成。
7.4.4 Protected
Protected与Private有些类似。在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。
在Protected部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。
7.4.5 Automated
C++的程序员可能对这个保留字比较陌生,在Automated部分声明的成员类似于在Public部分声明的成员,它们都是公共的,唯一的区别在于在Automated部分声明的方法和特性将生成OLE自动化操作的类型信息。
注意:Automated只适用于基类是TAuto0bject的类声明中,在Automated部分声明的方法,其参数和返回类型(如果是函数的话)必须是可自动操作的。在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可自动操作的,否则将导致错误。可自动操作的类型包括:Byte、Currency、Double、Integer、Single 、SmallInt、String、TDateTime、Variant、WordBool等。
在Automated部分声明的方法只能采用Register调用约定,方法可以是虚拟的但不能是动态的。在Automated部分声明的特性只能带Read和Write限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和Write指定的只能是方法而不能是字段,方法也只能采用Register调用约定,也不允许对特性重载。
在Automated部分声明的方法或特性分配一个识别号(ID),如果不带DispId限定符,编译器自动给方法或特性分配一个相异的Id,如果带DispId限定符,注意Id不能重复。
7.5 类类型的兼容性
一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码:
type
TScreenThing = Class
X,Y:Longint;
procedure Draw;
end;
T3DScreenThing = Class(TScreenThing)
Z:Longint;
end;
procedure ResetScreenThing(T:TScreenThing);
begin
T. X:=0;
U. Y:=0;
V. Draw;
end;
procedure Reset3DScreenThing(T:T3DScreenThing);
begin
T. X:=0;
T.Y:=0;
T.Z:=0;
U.Draw;
end;
var
Q:TScreenThing; R:T3DScreenThin;
begin
{...}
ResetScreenThing(Q);
ResetScreenThing(R); {this work}
Reset3DScreenThing(Q); { but this does not}
在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。
7.6 VCL类结构
我们介绍过的Delphi的VCL构件都是使用类类型定义的对象。在Delphi中,所有的类都是从一个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了一些操纵类的最基本的方法,是Delphi所有类的缺省祖先类。使用View|Browse命令,可以打开Browse Object命令,查看Delphi各对象之间的继承关系。
TObject类是一切构件类和对象的基类,位于继承关系的最顶层。TPersistent类是TObject类的下一级继承者,它是一个抽象类,主要为它的继承者提供对流的读写能力。
TComponent类是TPersistent类的下一级继承者,它是VCL中所有构件的祖先类。TComponent类定义了构件最基本的特性、方法和事件。尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有几个非可视的构件,如TTime构件和TDataSource构件等,绝大多数构件是从TComponent类的下级TControl类继承下来的,从TControl类继承下来的都是可视化的构件,这些构件也称为控制。TControl类定义了VCL中所有可视化构件基本的特性、方法和事件等。
TWinControl和TGraphicControl类都是TControl类的子类。TWinControl的子类主要是用于窗口控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗口,有自己的句柄,占用Windows资源,并且可以与用户交互。而TGraphicControl的子类没有窗口句柄,也不占用Windows资源类,也能接受键盘的输入,它们的主要优点在于节约资源,如TLabel和TSpeedButton等构件。