delphi(7)
时间:2006-06-10 来源:许我一个信仰
第8章 高级特性编程
对任何快速应用程序开发环境来说,特性都是必要的。在设计时需要可视化操纵属性来产生界面效果,简单的数据在这种工作环境下显得不够智能化。在Bjarne Stroustrop创建C++编程语言时对这一点可能有所了解,因为通过操作重载可以重定义对象的操作符。这样在C++程序中将对象作为左操作数或右操作数使用时,将会调用重载操作符方法。但对于设计时环境来说,这是个过于复杂的解决方案。要更好地管理数据,可以创建通过读写方法进行访问的数据。特性解决了该问题。Visual Basic可以处理设计时特性,但程序员无法控制特性读写方法的代码。Delphi是第一个可以对数据进行直观而一致的读写的程序。
术语特性把所有的东西都联系到了一起。消息和事件与Windows相联系,方法描述对象的行为,数据描述对象的状态,而特性则通过称之为特性存取限定符的标记方法对私有数据提供了受限的访问途径。避免破坏数据是C++作出的承诺之一。Delphi通过特性存取限定符实现了该承诺。如果类内部的数据使用是正确的,而且存取限定符限定了私有字段的访问方式,那么数据是不会被破坏的。
本章中,我们将仔细探索特性的方方面面。您将学到怎样编写数组特性、索引特性和虚特性。本章还将示范如何定义默认特性和存储值。当您学过本章后,将对Delphi的面向对象程序设计有一个完整的印象。
8.1 声 明 特 性
特性是数据的表示,它们就像是入口一样。通常特性表示私有数据,即Object Pascal术语中所说的字段。惯例是以F前缀命名私有字段,去掉F后则成为特性名。特性名的权限一般是公有或公开的。通常公开的特性是为组件保留的。特性声明的一般语法如下。
property PropertyName: DataType read ReadIdentifier write
WriteIdentifier;
关键字property是必须的。PropertyName通常是特性所表示的字段名去掉F前缀得到的。这样如果特性表示用于存储名字的数据,那么私有字段的名字是FName而特性的名字则为Name。如果字符串的数据类型是合适的,那么对于特性和字段来说DataType都是string。ReadIdentifier和WriteIdentifier可以是字段。下面是一个类,其中包含了定义正确的名字特性和字段特性。
type
TContact = class
private
FName : String;
public
property Name : String read FName write FName;
end;
当定义TContact类的实例并对Contact.Name进行写入,那么实际上修改了字段数据FName。上面的例子是对特性的最简单的使用。但与将FName定义为公有数据元素相比,上面的用法并未对FName提供任何额外的控制。
8.1.1 存取限定符
有时将特性解析到数据就足够了。其余情况下,您可能希望控制数据的读写方式。我们来考虑一个TStrings类型的特性。假定在联系消息中有电话号码列表,但这些号码并不总是使用。您可能希望对这些字符串定义惰性的实例。可以编写算法,使之具有如下的惰性实例化逻辑。
On read instantiate and initialize the list of phone numbers.
(在读取特性时,实例化并初始化电话号码的列表)
下面的类示范了该用法。
type
TContact = class
private
FName : String;
FPhoneNumbers : TStrings;
function CreatePhones : TStrings;
protected
function GetPhoneNumbers : TStrings;
procedure SetPhoneNumbers( const Value : TStrings );
public
constructor Create; virtual;
destructor Destroy; override;
property Name : String read FName write FName;
property PhoneNumbers : TStrings read GetPhoneNumbers write
SetPhoneNumbers;
end;
注意:在列出的代码中,可以看到声明了一个TStrings类型字段但却创建了TStringList类型的实例。TStrings是抽象类型,因此可以使用任何TStrings类的子类。许多现存的过程都需要TStrings类型的参数。这样如果声明了TStrings类型的数据,就可以与现存的过程相兼容;但访问TStrings的实例会导致EAbstractError异常。按照通常的规则,在可能的情况下可以把变量或参数声明为抽象超类,而传递子类的实例。
上述的Name特性是直接读写字段的。但如果Name对应的字段具有持久属性,那么我们可能需要定义一个write方法,在方法中将该对象标记为修改过的,以便可以将改变保存(到数据库或其他的持久存储设施)。PhoneNumbers特性示范了read和write方法。当使用object.PhoneNumbers时并不直接读写字段,若PhoneNumbers作为右值使用,则调用GetPhoneNumbers,如果PhoneNumbers作为左值使用,则调用SetPhoneNumbers。
使用读写方法意味着,当引用该特性时会调用相应的过程或函数。TContact新的实现版本示范了如何在第一次访问PhoneNumbers时创建其惰性实例。
implementation
constructor TContact.Create;
begin
inherited;
FPhoneNumbers := Nil;
end;
function TContact.CreatePhones : TStrings;
begin
if( Not Assigned(FPhoneNumbers)) then
FPhoneNumbers := TStringList.Create;
result := FPhoneNumbers;
end;
destructor TContact.Destroy;
begin
FPhoneNumbers.Free;
inherited;
end;
function TContact.GetPhoneNumbers: TStrings;
begin
result := CreatePhones;
end;
procedure TContact.SetPhoneNumbers(const Value: TStrings);
begin
if( Value = FPhoneNumbers ) then exit;
PhoneNumbers.Assign( Value );
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
with TContact.Create do
begin
PhoneNumbers := ListBox1.Items;
Free;
end;
end;
构造函数将电话号码字段初始化为空。即使只是将数据初始化为Nil,这也是个好习惯。当PhoneNumbers作为右值使用时,GetPhoneNumbers将调用CreatePhones;在该方法中,如果FPhoneNumbers仍然为Nil,将实例化一个TStringList赋值给FPhoneNumbers,然后返回对FPhoneNumbers的引用。不管怎样总是返回对FPhoneNumbers字段的引用。SetPhoneNumbers首先确认新的值并非只是引用了当前值,如果是这样的话将退出该方法。如果Value参数代表了一组新的号码,那么将调用PhoneNumbers.Assign( Value ),以确保参数字符串列表中的值能够正确地赋值给FPhoneNumbers字段。可以注意到在SetPhoneNumbers中的Assign方法使用了PhoneNumbers特性,而不是FPhoneNumbers字段。在SetPhoneNumbers方法中的PhoneNumbers特性属于右值用法,即调用了读方法,以确保电话号码字符串列表是存在的。
析构函数正确地清除了FPhoneNumbers中的字符串。Button1Click事件方法示范了如何把列表框中的字符串赋值给电话号码特性。如果您对控制流程仍不清楚,可以创建一个新的工程并选中Projects Options对话框中Compiler属性页上的Use Debug DCUs复选框,然后对步进跟踪每一行代码。
读存取限定符
读存取限定符总是一个函数,其返回类型与特性类型相同。按照惯例,该函数的名字前缀为Get,后接特性的名字,如下面的代码所示。PhoneNumbers特性的读存取限定符如下:
function GetPhoneNumbers : TStrings;
对于数组特性和索引特性,它们的读存取限定符函数有一个表示索引值的参数。更多的细节可以参见有关数组特性和索引特性的章节。
写存取限定符
写存取限定符总是有一个参数的过程。参数列表通常形如const Value : DataType,其中DataType与特性的类型相同。按照惯例,过程名的前缀为Set,后接特性的名字。因此PhoneNumbers的写存取限定符如下:
procedure SetPhoneNumbers( const Value : TStrings );
数组特性和索引特性的写存取限定符有两个参数。第一个参数表示索引,第二个表示新的值。更多的细节,可以阅读有关数组特性和索引特性的章节。
8.1.2 只读和只写特性的定义
有时候只用数据无法得到只读和只写属性。要使数据只读,可以只定义读方法;而要使数据只写,可以只定义写方法。只读特性与常数相似。类的用户无法修改只读特性,这确保了数据不会被不适当的修改。只写特性在技术上是可能的,但很少用到。
8.1.3 针对处理器密集型特性修改的安全措施
如果数据进行了不必要的更新,那么导致处理器密集型更新的数据将会浪费珍贵的CPU时间。类似于写入到数据库字段的文字,或写入到图像控件的Picture特性的图形,都可以在写方法进行检查,以确保只对新的或改变的数据进行更新。
TMyGraphicControl = class(TCustomControl)
private
FImage : TImage;
protected
function GetPicture : TPicture;
procedure SetPicture( const Value : TPicture );
public
constructor Create( AOwner : TComponent ); override;
destructor Destroy; override;
property Picture : TPicture read GetPicture write SetPicture;
end;
implementation
constructor TMyGraphicControl.Create(AOwner: TComponent);
begin
inherited;
FImage := TImage.Create(AOwner);
FImage.Parent := Self;
FImage.Align := alClient;
end;
destructor TMyGraphicControl.Destroy;
begin
FImage.Free;
inherited;
end;
function TMyGraphicControl.GetPicture : TPicture;
begin
result := FImage.Picture;
end;
procedure TMyGraphicControl.SetPicture(const Value: TPicture);
begin
if( FImage.Picture = Value ) then exit;
FImage.Picture.Assign(Value);
Repaint;
end;
上面代码中的SetPicture写方法首先检查Picture图形,以确保Value参数并非FImage.Picture的别名。不幸的是,这种类型的检查不能发现不同控件中的相同图像。代码需要比上面的相等测试更加精巧才行。如果TPicture对象并非已存在的同一对象,则调用Assign方法进行赋值,该方法调用TImage类中定义的SetGraphic写方法并创建新的TGraphic类型对象,然后使用图形类的构造函数来复制实际的图像。最后调用repaint方法,该方法向控件发出重新绘制的消息。
图8.1 使用P格式限定符来观察对象的地址
当测试代码以检查对自身的赋值时,可以使用如图8.1所示的Evaluate/Modify对话框来查看对象引用。在Expression域的末尾添加逗号和P(,P),即可显示对象的地址(见图8.1)。如果两个地址相同,那么两个引用指向的是同一对象。Delphi为Evaluate/Modify对话框提供了各种格式限定符,可以在Result域中格式化数据(表8.1包含了格式限定符的完整列表)。
表8.1 图8.1中显示的Evaluate/Modify对话框的格式
化字符,使您可以用更有意义的方式查看数据
限定符 |
影响类型 |
描述 |
,C |
字符和字符串 |
以Pascal中的#number格式显示0至31的ASCII字符 |
,S |
字符和字符串 |
以Pascal中的#number格式显示0至31的ASCII字符 |
,D |
整数 |
以十进制格式显示整数值 |
,H或,X |
整数 |
以十六进制格式显示整数值,前缀为$ |
,Fn |
浮点数 |
显示n个有效比特位,其中2 <= n <= 18。默认情况下n为11 |
,P |
指针 |
显示32位地址 |
,R |
记录或对象 |
显示记录或对象的属性,包括字段名和值 |
,nM |
所有类型 |
显示n比特长的内存转储。默认以两位十六进制值进行格式化。将nM与C、D、H、X或S一同使用可以改变数据的格式 |
在图8.1的Evaluate/Modify对话框中使用格式限定符,可以使用对解决问题最有意义的方式观察数据。将代码编辑器的光标置于要观察的数据上,按键Ctrl+F7即可打开Evaluate/Modify对话框。可以计算或修改简单的数据或对象,也可在Expression域中写出复杂的表达式并观察其值。
8.1.4 使用Assign方法进行对象赋值
对象经常需要深复制,以确保将字段值正确地赋值给目标对象的字段。TPersistent类引入了Assign和AssignTo方法可以提供深复制的行为。当进行对象赋值时,首先要决定是需要对象的引用还是对象的副本。使用赋值操作符(:=)将一个引用赋值给一个对象。使用Assign方法可以将对象属性进行深复制。
如果进行引用赋值,那么并不需要对象的实例,只需变量引用即可。考虑下列代码:
var
A, B : TAssignableObject;
begin
A := TAssignableObject.Create;
B := TAssignableObject.Create;
A := B; // error, B's object is lost causing a memory leak
end;
上面列出的代码导致了内存泄漏。在Delphi中发生内存泄漏的可能性比C++要小,但仍然可以发生。在C++的术语中,这种类型的内存泄漏称之为slicing problem。应去掉创建A的语句,或在将B赋值给A之前释放A所引用的内存。A := B意味着A是B的引用,即二者是同一对象。如果写A.Assign( B ),那么A就是一个独立的对象,但其状态与B相同。
8.2 特性的存储限定符
存储限定符决定了如何维护运行时类型信息。具体的说,在保存窗体或数据模块时,存储限定符default、nodefault和stored决定了哪些公开特性将存储在.DFM文件中。当Delphi保存窗体时,它会检查组件的状态,并基于property语句中给出的存储限定符来对公开特性进行存储。
unit UDemoStorage;
// UDemoStorage.pas - Contains several variations of storage specifiers
// Copyright (c) 2000. All Rights Reserved.
// by Software Conceptions, Inc. Okemos, MI USA (800) 471-5890
// Written by Paul Kimmel
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs;
type
TDemoStorage = class(TComponent)
private
{ Private declarations }
FAnInteger : Integer;
FAString : String;
FADouble : Double;
FASingle : Single;
FColor : TColor;
FFontStyles : TFontStyles; //set
function IsStored : Boolean;
function ImplicitDefault : Boolean;
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
constructor Create( AOwner : TComponent ); override;
property AnInteger : Integer read FAnInteger write FanInteger
stored True default 13;
property AString : String read FAString write FAString
stored ImplicitDefault;
property ADouble : Double read FADouble write FADouble
stored True;
property ASingle : Single read FASingle write FASingle
stored IsStored;
property Color : TColor read FColor write FColor default clBlue;
property FontStyles : TFontStyles read FFontStyles write
FFontStyles stored True default [fsItalic, fsBold];
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('PKTools', [TDemoStorage]);
end;
{ TDemoStorage }
constructor TDemoStorage.Create(AOwner: TComponent);
begin
inherited;
FAnInteger := 13;
FColor := clBlue;
FFontStyles := [fsItalic, fsBold];
end;
function TDemoStorage.ImplicitDefault: Boolean;
begin
if( FAString = EmptyStr ) then
FAString := 'Default';
result := True;
end;
function TDemoStorage.IsStored: Boolean;
begin
result := FASingle > 0.0;
end;
end.
上面的代码示范了存储限定符的各种不同的组合方式,在阅读本节时可以参考。
8.2.1 默认和非默认存储方式的使用
默认情况下,如果给出了存储限定符,那么只有在值被修改的情况下才对公开特性的值进行存储。这里有几个从本节开头的代码中摘取的例子,它们使用了默认存储限定符。
property AnInteger : Integer read FAnInteger write FanInteger
stored True default 13;
property Color : TColor read FColor write FColor default clBlue;
property FontStyles : TFontStyles read FFontStyles write
FFontStyles stored True default [fsItalic, fsBold];
提示:只有在指定了nodefault限定符并且stored值为True时,或指定了default限定符而修改后的值与默认值不同时,才会存储特性值。程序员负责在对象的构造函数中进行默认赋值。
第一个特性AnInteger将被存储,其默认值为13。考虑下面对一个.DFM文件的摘录,其中包含了一个DemoStorage对象,它将绘制在窗体上而且不会改变。
object DemoStorage2: TDemoStorage
AString = 'Default'
Left = 120
Top = 64
end
考虑AnInteger特性,默认值13表示构造函数将把AnInteger初始化为13,而后该特性将从DFM文件中读取。默认值并不会自动地赋值给特性,需要程序员完成该工作。存储限定符default 13意味着如果值不是13,就存储该特性。请注意Color的默认值为clBlue,而Color特性并未指定stored限定符。现在将Color特性改变为clRed,查看.DFM文件的对应片段。
object DemoStorage2: TDemoStorage
AString = 'Default'
Color = clRed
Left = 120
Top = 64
end
现在可以看到Color特性被存储了。构造函数定义为将clBlue赋值给Color,而当读取.DFM文件时,将修改Color来包含存储的值。您需要在构造函数中将特性初始化为默认值。这样减少了存储在DFM文件中的信息量。
如FontStyle集合特性所示,您可以为集合设置默认值。Delphi只对有序类型和集合直接支持默认值。这样在AString、ADouble和ASingle等特性声明中不能使用default限定符。但可以标识出这些值是否进行存储。默认情况下,实数、指针和字符串将初始化为相应的空值形式,只有在Object Inspector中出现非空值时才进行存储。
object DemoStorage2: TDemoStorage
AString = 'Default'
ASingle = 0.300000011920929
Color = clRed
Left = 120
Top = 64
end
例如在Object Inspector中,在ASingle的编辑域写入.3,那么在.DFM文件中将包含ASingle特性(参见前面的DFM文件片段)。
8.2.2 使用stored限定符
如果不指定存储限定符,那么公开特性只有在Object Inspector中的值不等于空值时,才进行存储。对于字符串,是空字符串;对于指针,是Nil;对于实数,是0。可以在关键字stored之后放置True或False或返回布尔值的函数来表示是否存储该值。
property AnInteger : Integer read FAnInteger write FanInteger
stored True default 13;
property AString : String read FAString write FAString
stored ImplicitDefault;
property ADouble : Double read FADouble write FADouble
stored True;
property ASingle : Single read FASingle write FASingle stored IsStored;
property Color : TColor read FColor write FColor default clBlue;
property FontStyles : TFontStyles read FFontStyles write
FFontStyles stored True default [fsItalic, fsBold];
只有在Object Inspector中AnInteger的值不是13时,才会存储AnInteger的值(我们稍后再回到AString和ASingle)。只有当ADouble的值不是0时才存储。默认情况下,在Color的值不是clBlue时才进行存储。当FontStyle的值不是[fsItalic, fsBold]时才存储。如果在FondStyle的stored限定符后指定了False,那么无论是否在Object Inspector中设置了值,其值总是[fsItalic, fsBold]。即该值不会持久化。
AString和ASingle特性使用函数来决定是否进行存储。
function TDemoStorage.ImplicitDefault: Boolean;
begin
if( FAString = EmptyStr ) then
FAString := 'Default';
result := True;
end;
function TDemoStorage.IsStored: Boolean;
begin
result := FASingle > 0.0;
end;
提示:要对有序类型和集合以外的特性模拟默认值,可以使用存储函数来动态地设置其值。您不需要在构造函数中设置这些值,它们会从特性流中读出。
IsStored函数没有参数,返回一个布尔值(这是存储限定符函数所必须的格式)。如果FASingle是非负的,存储函数将返回True。
如果AString为EmptyStr,ImplicitDefault方法将为AString定义一个实际值。由于AString的存储函数的定义方式,该特性总是被存储的,如果AString为空字符串则‘Default’将存储在特性值中(参见前面的窗体文件的片段,可以看到AString的值)。这与在构造函数中向AString赋值‘Default’是等价的。不同之处在于,如果没有特性存储函数,在Object Inspector中特性的默认值看起来是空字符串,而实际上其值为‘Default’。原因是特性是从.DFM文件流中读出的,而没有调用构造函数。
8.3 定义数组特性
数组特性表示了可索引的数据,其索引方式与内建的数组相似。实际的数据以TList和TStrings等类型更为常见;但也可以是内建的数组,通过存取方法来确保索引值位于有效范围内。由于数组特性也使用同样的get和set方法,可以为需要通用接口访问的数据定义数组特性。
property PropertyName[ Index : IndexType ] : PropertyType read
GetPropertyName write SetPropertyName; [default;]
按照惯例,在存在实际字段的情况下,PropertyName是实际字段名去掉F前缀,这与其他特性的命名惯例相同。IndexType可以像内建数组一样使用有序类型,但索引并不只限于有序值。PropertyType与实际字段的类型相同。例如,如果实际字段是整数数组,特性类型就是整数。数组特性的存取限定符必须是方法。
注意:对于以常量字符串值来索引的数组特性的例子,请参见TString类中Values特性的声明:property Values[const Name : string ] : string;。
读方法定义为函数,有一个参数,类型与特性声明中的索引限定符相同,返回与特性类型相同的数据。写方法有两个参数,索引参数和新值。假定某个类中的整数数组特性名为Integers,其类声明可能如下:
type
TIntegers = class
private
FIntArray : array[1..10] of integer;
procedure CheckRange( Index : Integer );
function GetIntArray(Index: Integer): Integer;
procedure SetIntArray(Index: Integer; const Value: Integer);
public
property IntArray[ Index : Integer ] : Integer
read GetIntArray write SetIntArray; default;
end;
FIntArray是实际的字段值。TIntegers表示由整数构成的智能数组。代表整数数组的特性定义为IntArray,索引为整数,读方法为GetIntArray,写方法定义为SetIntArray。如上所述,读方法的惟一参数是索引值,而写方法的两个参数中,索引值在前,新的值定义为常量。
实现中使用了CheckRange过程,该过程确认索引在数组的上下界之间。
implementation
procedure TIntegers.CheckRange(Index: Integer);
begin
if( Index < Low(FIntArray) ) or (Index > High(FIntArray)) then
raise ERangeError.CreateFmt( 'Index %d exceeds array bounds',
[Index] );
end;
function TIntegers.GetIntArray(Index: Integer): Integer;
begin
CheckRange( Index );
result := FIntArray[Index];
end;
procedure TIntegers.SetIntArray(Index: Integer; const Value: Integer);
begin
CheckRange(Index);
FIntArray[Index] := Value;
end;
实现是很简单的。每个存取函数都调用CheckRange,当索引过界时该函数将引发异常。从外部看来,TIntegers类型的对象就像是不加修饰的数组,而其工作方式与本地数组也很相似,但更加安全。
下面的例子演示了本地数组与TIntegers数组类之间的不同。
var
A : array[1..10] of integer;
Integers : TIntegers;
I : integer;
begin
{$R-}
I := 11;
A[I] := 100;
ShowMessage( InttoStr(A[I]) );
Integers := TIntegers.Create;
try
Integers[11] := 100;
ShowMessage( IntToStr( Integers[11] ));
finally
Integers.Free;
end;
{$R+}
end;
在列出的代码中,$R-编译器指令移去了范围检查,这代表了您的程序将进入的状态。变量部分定义了整数数组A。代码A[I]不会引发错误,但因为A[11]并不存在,实际上它重写了内存。该代码将导致奇怪的、不可靠的行为。而Integers实例则工作正确,不管编译器指令是否存在。将11传递给Integers对象将引发ERangeError,从而避免了内存重写。
8.3.1 数组特性的default限定符
本节的开头定义了一个类TIntegers,它包含了一个整数数组,而无须特别命名对应的数组特性。这可以通过default限定符来完成(参见下面列出的代码)。
property IntArray[ Index : Integer ] : Integer read GetIntArray
write SetIntArray; default;
default限定符意味着,在相应的上下文环境中如果不指明,就使用该特性;如果不使用default限定符,就必须写出Integers.IntArray[I]才能索引IntArray特性。Default限定符使得编译器将Integers[I]解析到Integers.IntArray[I],然后将根据Integers用作左值还是右值来调用存取函数。
把Integers用作右值的代码如下。
var
Value : Integer;
begin
Value := Integers[5];
end;
上面的代码将解析到Value := Integers.GetIntArray( 5 )。如果把代码改为Integers[5] := Value,就是把Integers作为左值使用,将解析到Integers.SetIntArray( 5, Value )。
8.3.2 隐式范围检查
通过使用仔细定义的有序类型作为索引限定符,可以向数组特性加入隐式范围检查。在TIntegers中,使用1..10定义了数组的大小。通过定义指定范围的索引类型,而不是直接使用整数类型,可以减少使用CheckRange的可能性。这样,编译器将捕获该错误。
type
TIntegerRange = 1..10;
TIntegers = class
private
FIntArray : array[TIntegerRange] of integer;
function GetIntArray(Index: TIntegerRange): Integer;
procedure SetIntArray(Index: TIntegerRange; const Value:
Integer);
public
property IntArray[Index: TIntegerRange] : Integer read
GetIntArray
write SetIntArray; default;
end;
提示:您可能希望写出由编译器捕获错误的代码。编译时错误比运行时错误易于解决。通过仔细定义的类型化变量和强类型化的过程参数,即可在编译时捕获错误。
为了在数组特性的数组、存取方法和索引限定符中使用TIntegerRange,对TIntegers类进行了重定义。您仍然可以使用整数值对TIntegers对象进行索引,但如果索引值过界编译器将捕获该错误。您也可以使用枚举类型作为索引限定符。
8.4 定义索引特性
索引特性是逆转的数组特性。从外部看来,索引特性并不是类似数组的单个特性,而是与多个具有相同的读写方法的特性相似。索引是与特性连起来表示的。
property PropertyName : DataType index ordinal GetPropertyName
write SetPropertyName;
property PropertyName2 : DataType index ordinal + n GetPropertyName
write SetPropertyName;
假定在实际的同一数据存储结构中包含了两个命名特性,但看起来却是独立的数据。上面的声明示范了两个指向同一数据容器的索引属性。
我们花一些时间来重新看一下本章开头处的TContact类。TContact类的定义中包含了三个元素:名字、电话和电子邮件,都是字符串类型。可以使用索引属性来访问每个字符串的实际数据值。
type
TNameRange = 0..2;
TContactRevisited = class
private
FData : TStrings;
protected
function GetData( Index : Integer ) : String;
procedure SetData( Index : Integer; const Value : String );
public
constructor Create; virtual;
destructor Destroy; override;
property Name : String index 0 read GetData write SetData;
property PhoneNumber : String index 1 read GetData write SetData;
property EMail : String index 2 read GetData write SetData;
end;
对每个特性的使用都解析到对GetData和SetData方法的调用。FData属性是一个TStrings对象,它是一个关联数组,可以用‘名字=值’对的形式存储数据。
修改过的联系信息类的实现代码如下列出。
const
NAMES : array[TNameRange] of String = ('Name', 'PhoneNumber',
'Email' );
constructor TContactRevisited.Create;
begin
inherited Create;
FData := TStringList.Create;
end;
destructor TContactRevisited.Destroy;
begin
FData.Free;
inherited;
end;
function TContactRevisited.GetData(Index : Integer): String;
begin
result := FData.Values[NAMES[Index]];
end;
procedure TContactRevisited.SetData(Index: Integer; const Value: String);
begin
FData.Values[NAMES[Index]] := Value;
end;
Values特性使用一个字符串名,然后从FData返回值,考虑一下SetData方法,该方法使用了NAMES特性,其参数为新的值以及索引值0,NAMES[0]值为'Name'。而FData.Values['Name']:=Value将查找以'Name='开头的字符串项,然后将其值设置为参数所表示的新值。
注意:由于索引值是在编译时静态定义的,因此只有在实际的数据容器的大小发生改变时,才有可能出现错误。
在TContactRevisited类中没有实际的字段值。实际的值存储在TStrings对象中。因为对于TStrings中的所有对象,代码都是相同的;所以无论对于两个或十个特性,两个读写方法就足够了。如果get和set方法比上面的代码更为复杂,那么将节省更多的代码。
8.4.1 使用枚举索引值
索引特性的索引限定符必须是整数值,而且要位于整数类型的上下界之间(-2147483647和2147483647之间)。但也可以使用枚举值作为索引限定符的值,这样就把可能值的范围限制到枚举类型中定义的那些值。对TContactRevisited类进行合适的修改,即可示范该技术。
// include typinfo in the uses statement!!!
{$M+}
TName = (Name, PhoneNumber, EMail);
{$M-}
TContactRevisited = class
private
FData : TStrings;
protected
function GetName( Name : TName ) : String;
function GetData( Index : TName ) : String;
procedure SetData( Index : TName; const Value : String );
public
constructor Create; virtual;
destructor Destroy; override;
property Name : String index Name read GetData write SetData;
property PhoneNumber : String index PhoneNumber read GetData
write SetData;
property EMail : String index Email read GetData write
SetData;
end;
通过把枚举类型TName作为索引值,可以使用有意义的名字。我们也修改了读写方法GetData和SetData,现在其参数为TName类型的索引限定符。现在已经不再需要字符串数组NAMES了。在GetData方法中加入了GetEnumName函数,该函数定义在typinfo.pas单元中,它使用RTTI信息来返回枚举项的字符串值。
再进行一些微小的改变,即可完成对TContactRevisited类的修改。
constructor TContactRevisited.Create;
begin
inherited Create;
FData := TStringList.Create;
end;
destructor TContactRevisited.Destroy;
begin
FData.Free;
inherited;
end;
function TContactRevisited.GetName( Name : TName ) : string;
begin
result := GetEnumName( TypeInfo(TName), Ord(Name ));
end;
function TContactRevisited.GetData(Index : TName): String;
begin
result := FData.Values[ GetName( Index ) ];
end;
procedure TContactRevisited.SetData(Index: TName; const Value: String);
begin
FData.Values[ GetName( Index ) ] := Value;
end;
在修改后的版本中,不再索引数组NAMES来得到名字,而是调用GetName。GetName方法使用RTTI信息将枚举值转换为字符串。得到的结果字符串用来索引TStrings类的数组特性Values。
8.5 多 态 特 性
使用方法作为存取限定符,既简单又清楚。通过将读写方法定义为虚方法(或抽象方法,我们在第7章那样做过),可以创建虚特性。虚特性的状态将以动态的方式进行修改,这取决于对象的实际实例。这里有一个基本的例子,演示了这方面的技术。
TVirtualProperty = class
private
FSomeData : String;
protected
function GetSomeData : string; virtual;
procedure SetSomeData( const Value : String ); virtual;
public
property SomeData : String read GetSomeData write SetSomeData;
end;
TSubclass = class(TVirtualProperty)
protected
function GetSomeData : string; override;
procedure SetSomeData( const Value : String ); override;
end;
TVirtualProperty类中定义了特性SomeData,通过读方法GetSomeData和写方法SetSomeData进行访问。两个存取方法都是虚方法。TSubClass子类化了TVirtualProperty类,使用override指令重载了GetSomeData和SetSomeData方法。
如果同时需要继承而来的行为和新的行为,可以在GetSomeData和SetSomeData方法的子类实现中调用方法的继承版本。如果需要全新的行为,不调用继承版本的方法即可。下面列出了完整的代码,包含了两个类的实现和对类的使用的例子。
unit UVirtualProperties;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
Procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TVirtualProperty = class
private
FSomeData : String;
protected
function GetSomeData : string; virtual;
procedure SetSomeData( const Value : String ); virtual;
public
property SomeData : String read GetSomeData write SetSomeData;
end;
TSubclass = class(TVirtualProperty)
protected
function GetSomeData : string; override;
procedure SetSomeData( const Value : String ); override;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{ TVirtualProperty }
function TVirtualProperty.GetSomeData: string;
begin
result := FSomeData;
end;
procedure TVirtualProperty.SetSomeData(const Value: String);
begin
FSomeData := Value;
end;
{ TSubclass }
function TSubclass.GetSomeData: string;
begin
result := inherited GetSomeData;
end;
procedure TSubclass.SetSomeData(const Value: String);
begin
if( SomeData = Value ) then exit;
inherited SetSomeData( Value );
end;
procedure TForm1.FormCreate(Sender: TObject);
var
VP : TVirtualProperty;
begin
VP := TSubClass.Create;
try
VP.SomeData := 'Follow me';
ShowMessage( VP.SomeData );
finally
VP.Free;
end;
end;
end.
图8.2 从ShowMessage(VP.GetSomeData)一行调用
栈的情况,此时ShowMessage对话框尚未显示
TVirtualProperty.GetSomeData返回实际的字段值FSomeData。TSubClass.GetSomeData是用超类方法实现的,它调用了继承而来的方法。TVirtualProperty.SetSomeData将实际的字段值设置为Value参数。在TSubClass.SetSomeData方法中,如果字段值与参数值相等,该方法将退出,否则将使用继承的方法来设置数据(即将显示ShowMessage对话框之前的调用栈情况,可以参照图8.2)。在FormCreate事件方法中声明了一个TVirtualProperty类型的变量,并将其初始化为子类TSubClass。从调用栈中可以清楚地看到,先调用了子类的方法,该方法又调用了超类的实现。
一般的,最好的习惯是子类化已存在的类来定义新的行为。这样减少了改变和重新测试已有代码的可能性,又可以在一个类中同时使用旧的和新的行为。本节中列出的代码示范了在子类中对超类特性的一个很小的改变。而有用的修改不必是很大的修改。该代码也示范了该技术的使用,在子类的特性存取方法中新增行为的复杂或简单与否,是依赖于您的需要的。
8.6 提升子类中特性的可见性
Delphi中的VCL库按照惯例,把许多组件类都按两阶段进行声明。名为TCustomClass的类定义了所有的属性,而把特性声明为保护权限的。然后把另一个类定义为TCustomClass类的子类,并将保护特性的可见性提升为公有或公开。例如TCustomEdit只定义了一个公开特性TabStop。而TEdit类则定义为该类的子类,将所有实现者所需的特性都提升到公有或公开访问区域。
TEdit = class(TCustomEdit)
published
property Anchors;
property AutoSelect;
property AutoSize;
property BevelEdges;
property BevelInner;
property BevelKind;
property BevelOuter;
property BiDiMode;
property BorderStyle;
property CharCase;
property Color;
property Constraints;
property Ctl3D;
property DragCursor;
property DragKind;
property DragMode;
property Enabled;
property Font;
property HideSelection;
property ImeMode;
property ImeName;
property MaxLength;
property OEMConvert;
property ParentBiDiMode;
property ParentColor;
property ParentCtl3D;
property ParentFont;
property ParentShowHint;
property PasswordChar;
property PopupMenu;
property ReadOnly;
property ShowHint;
property TabOrder;
property TabStop;
property Text;
property Visible;
property OnChange;
property OnClick;
property OnContextPopup;
property OnDblClick;
property OnDragDrop;
property OnDragOver;
property OnEndDock;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnStartDock;
property OnStartDrag;
end;
警告:如果重新定义了特性,即在子类中重新定义了完整的特性语句,可能会隐藏超类的存取方法或改变特性的可访问性。要提升可见性,可以使用property关键字和特性名重新声明特性。
组件面板上实际是VCL库中的TEdit控件。从列出的代码可以看出,TEdit只包含了提升的特性。考虑一个特性AutoSelect,它定义在TCustomEdit类里,在StdCtrls.pas单元中。
property AutoSelect: Boolean read FAutoSelect write FAutoSelect default True;
这里是该特性实际的定义。TEdit中的property语句只是提升了可见性而已。规则就是,如果要提升可见性,只需使用关键字property和特性名即可,而且只要定义该特性一次。
8.7 小 结
第8章涵盖了特性的有关知识,它是面向对象Pascal的巅峰。特性使对象更加易用,可以定义一些方法来受限地访问私有数据。公开特性将显示在Object Inspector中,这对于创建定制组件是必需的。接下来,您将学习怎样创建组件。
特性对于面向对象编程是必需的,因为它们表示了智能的数据。本章中的技术在本书其余部分用得很多,可以用来定义存取方法、定义数组和索引特性、创建多态特性等。