delphi 对象与类类型

来源:互联网 发布:红蚂蚁网络与阿里巴巴 编辑:程序博客网 时间:2024/06/05 10:31
     对象与类类型

  从用户角度考虑,用户并不需要了解面向对象编程,就可编写Delphi应用程序。当用户在建立新窗体,添加新构件以及处理事件时,大部分相关代码会由Delphi自动产生。但是,知道语言及其细节,对理解Delphi正在做什么并完全掌握它是很有帮助的。特别是有关类类型的概念及用法是学习后面构件编程及Delphi深入编程的基础。
  面向对象的编程语言基于三个基本概念:类、继承及多态性。类实际上是用户自定义数据类型之一,只是因为增加了方法、继承、多态等要素,从而在面向对象的编程中变得十分重要。类是Object Pascal面向对象编程的基础。类类型具有可继承性,被继承的类我们称为祖先类(基类),继承下来的类我们称为派生类(子类)。
  本章就有关类类型的概念及用法进行较为详细的介绍。
7.1 类类型和对象
  类和对象是两个常用的术语,然而经常容易混淆,因此需要区别它们的定义。
  类与本书Pascal语言部分介绍的记录类似,也是包含有各种域的结构化数据类型,然而,类包含方法。方法就是操纵对象域数据的过程或函数。类类型把数据和方法封装在一起。
  对象是类的实例(instance),即由类定义的数据类型的变量。对象是实体,当程序运行时,对象为它们的内部表达占用一些内存。对象与类的关系就象变量与类型的关系。
  在Object Pascal中,声明类数据类型使用保留字Class。下面是类类型声明的语法规则(图7.1)。
  其中,heritage部分指出类的直接祖先类,其语法规则如图7.2。
  从以上语法示意图可以看出,类类型可以指定一个祖先类型,表示该类型是从这个指定的祖先类型继承下来的,例如:
  Type TPoint=Class(TObject)
  上例中,声明了一个名叫TPoint的类类型,该类型是从类TObject继承下来的。注意:类类型的标识符一般以T打头,以区别于其它数据类型。如果声明中省略了heritage部分,表示该类型是继承缺省的祖先类TObject,例如: 
Type TPoint=Class
  Component List部分列出类的成员列表。从语法示意图还可以看出,类类型声明中可以没有成员列表,也可以有,其语法规则如图7.3。
  可以看出,类型可以有3类成员,分别是Field(字段)、Method(方法)、property(特性)。
  类类型中的字段也就是类的数据部分,其声明语法同记录中字段的声明语法相似,其语法规则如图7.4。
  其中字段的类型可以是各种数据类型,甚至是另一个类类型。Visibility Specifier部分定义构件成员的可见性。在Object Pascal中,类成员的可见性是通过这么下面几个保留字来定义的:Published,Public,Protected,Private,Automated。
  一个典型的类类型示例如下:
TDate = Class 
private 
 Month,Day,Year:Integer;
public 
 constructor Create; 
 destructor Destroy;
  override; 
  Procedure SetValue(m,d,y:Integer);
  Function LeapYear:Boolean; 
end;
  在Tdate类中,声明了三个字段, Month,Day,Year,数据类型是Integer,并且声明了一个构造Create,一个析构Destroy,一个函数LeapYear,其返回类型是Boolean,一个过程SetValue。其中三个字段,Month,Day,Year的可见性为Private表示只有类型本身的函数或过程可以访问,而在Public部分定义的函数是使用该方法的对象都可以访问的。
  注意:跟其它数据类型不同的是,类类型的声明只能出现在Program单元或UNIT单元最外层作用域的类型定义部分,而不能定义在变量说明部分或一个过程或函数内。因此,类类型的作用域总是全局的。
  在类类型中声明的方法实际上是向前的定义(forward declaration ),因为必须在类类型定义之后,在同一模块的其它某个地方声明,即在程序的Implementation区进行对该方法进行定义。例如,上面类的方法定义为:
implementation
{$R *.DFM}
procedure TDate.SetValue(m,d,y:integer);
begin
 Day:=d; Year:=y;
end;
function TDate.LeapYear:Boolean;
begin 
if (Year mod 4<>0) then LeapYear:=False 
else if (Year mod 100<>0) then LeapYear:=True 
else if (Year mod 400<>0) then LeapYear:=False
else LeapYear:=True;
end;
  访问对象中的各个成员,跟访问记录变量中的字段类似,是用对象名加一个小圆点和成员的名,例如:
Var ADay:TDay; 
  Leap:Boolean;
begin 
...
ADay.SetValue(10,10,1997); 
Leap:=ADay.LeapYear;
end;
另外,你可以使用With语句简化对类成员的访问.
7.2 类 的 方 法
 方法(Method)是在一个对象上执行指定操作的过程或函数。方法类似于过程或函数,但方法的操作范围只能是对象内部的数据或对象可以访问的数据。因此在使用方法时,必须指定该方法的对象名。
7.2.1 方法的声明和定义
  声明一个方法的语法同声明一个过程或函数的语法相似,其中,method heading定义方法的首部,从语法示意图中可以看出,方法分为4种类型,分别是构造、析构、过程和函数,它们分别用Constuctor、Destructor、Procedure、Function这4个符号来声明。
  在定义方法时,可以直接使用类中已声明的字段,不需要作为参数来传递,访问这些字段时也不需要引用限定符。例如,TDate的Month字段。
  跟普通的过程或函数一样,调用方法时要注意形参和实参以及返回类型的匹配。不过在调用方法时,Object Pascal隐含传递了一个参数Self,这个参数是一个指向输出方法的对象实例的指针,相当于C++里的This指针。
7.2.2 构造和析构
  构造(Constructors)和析构(Destructors)是类类型中两种特殊的方法,用于控制类对象创建及初始化,及删除时的行为。一个类可以没有也可以有多个构造和析构,构造和析构也可以被继承。
  熟悉C++的程序员都知道,在C++中,当用一个类类型声明一个对象时,将自动调用类的构造函数(这就是C++中一般不需要显式调用构造函数的原因),而在Object Pascal中,当声明了一个类类型的变量后,并没有在内存中建立对象,必须调用类的构造手工创建对象,至少对于用户自己定义的类对象。用户放置在窗体上的构件范例可以由Delphi自己建立。
1.构造
  构造用建立对象,并对对象进行初始化。通常,当调用构造时,构造类似一个函数,返回一个新分配的并初始化了的类类型实例。
  构造跟一般的方法不同的是,一般的方法只能在对象实例中引用,而构造既可以由一个对象实例引用,也可以直接由类来引用。当用类来引用类的构造时,实际上程序做了以下工作:
 (1)首先在堆中开辟一块区域用于存贮对象。
 (2)然后对这块区域缺省初始化。初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为Nil,字符串类型的字段清为空等。
 (3)执行构造中用户指定的动作。
 (4)返回一个新分配的并初始化了的类类型实例。返回值的类型必须就是类的类型。
  当你用在对象实例中引用类的构造时,构造类似一个普通的过程方法。这意味着一个新对象还没有被分配和初始化,调用构造不返回一个对象实例。相反,构造只对一个指定的对象实例操作,只执行用户在构造语句中指定的操作。
  例如,在创建一个新的对象时,尽管还没有对象实例存在,仍然可以调用类的构造,程序示例如下:
type 
TShape = class(TGraphicControl) 
private 
 FPen: TPen; 
 FBrush: TBrush; 
 procedure PenChanged(Sender: TObject); 
 procedure BrushChanged(Sender: TObject); 
public 
 constructor Create(Owner: TComponent);
 override; 
  destructor Destroy;
  override; ...
end;
constructor TShape.Create(Owner: TComponent);
begin 
  inherited Create(Owner);{ Initialize inherited parts } 
  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等构件。
0 0