`

Classes and objects(类和对象)

阅读更多

Classes and objects(类和对象)


类(或者类类型)定义了一个结构,它包括字段(也称为域)、方法和属性;类的实例叫做对象;类的字
段、方法和属性被称为它的部件(components)或成员。
• 字段在本质上是一个对象的变量。和记录的字段类似,类的字段表示一个类实例的数据项;
• 方法是一个过程或者函数,它和类相关联。绝大多数方法作用在对象(也就是类的实例)上,其它
一些方法(称为类方法)作用在类上面。
• 属性被看作是访问对象的数据的接口,对象的数据通常用字段来存储。属性有存取设定,它决定数
据如何被读取或修改。从程序的其它地方(在对象本身以外)来看,属性在很大程度上就像一个字
段(但本质上它相当于方法,比如在类的实例中并不为它分配内存)。
对象被动态分配一个内存块,内存结构由类类型决定。每个对象拥有类中所定义的每个字段的唯一拷贝,
但一个类的所有实例共享相同的方法。对象分别通过称为构造函数和析构函数的方法创建和销毁。
一个类变量实际是一个指针,它引用一个对象(称它为对象引用),所以,多个变量可以指向同一个对象。
像其它指针一样,一个类变量的值可以是nil。虽然类变量是一个指针,但我们直接用它来表示一个对象,
例如,SomeObject.Size := 100 是把对象的Size 属性设为100,你不能用下面的命令给它赋值:
SomeObject^.Size := 100。


Class types(类类型)
类类型必须在实例化之前进行声明并给定一个名称(不能在变量声明中定义一个类类型),你只能在程序
(program)或单元(unit)的最外层声明类,而不能在过程或函数中声明。
一个类声明有如下格式
type className = class (ancestorClass)
memberList
end;
这里,className 是任何有效标志符,(ancestorClass)是可选的,memberList 声明类的各成员,也就是
它的字段、方法和属性。若你省略了(ancestorClass),则新定义的类直接继承自内置的类TObject。如
果包含(ancestorClass)并且memberList 是空的,你可以省略end。一个类声明也可以包括它实现的接
口列表,请参考Implementing interfaces。
在类声明中,方法看起来就像函数(或过程)头,而没有函数(或过程)体。方法的定义出现在程序的
其它地方。
比如,这里是Classes 单元中TMemoryStream 类的声明
type
TMemoryStream = class(TCustomMemoryStream)
private
FCapacity: Longint;
procedure SetCapacity(NewCapacity: Longint);
protected
function Realloc(var NewCapacity: Longint): Pointer; virtual;
property Capacity: Longint read FCapacity write SetCapacity;
public
destructor Destroy; override;
procedure Clear;
procedure LoadFromStream(Stream: TStream);
procedure LoadFromFile(const FileName: string);
procedure SetSize(NewSize: Longint); override;
function Write(const Buffer; Count: Longint): Longint; override;
end;
TMemoryStream 是TStream(在Classes 单元中)的后代,继承了它的大部分成员,但又定义(或重新定
义)了几个方法和属性,包括它的析构(destructor)函数Destroy。它的构造函数Create 从TObject 继承,
没有任何改变,所以没有重新声明。每个成员被声明为private、protected 或者public(这个类没有published
成员),关于这些术语的解释,请参考Visibility of class members。
给定上面的声明,你可以像下面一样创建TMemoryStream 的一个实例:
var stream: TMemoryStream;
stream := TMemoryStream.Create;


Inheritance and scope(继承和作用域)
当你声明一个类时,可以指定它的父类,比如,
type TSomeControl = class(TControl);
定义了一个叫做TSomeControl 的类,它继承自TControl。一个类自动从它的父类继承所有的成员,且可
以声明新成员,也可以重新定义继承下来的成员,但不能删除祖先类定义的成员。所以,TSomeControl
包含了在TControl 以及它的每个祖先中定义的所有成员。
类成员标志符的作用域开始于它声明的地方,直到类声明的结束,并且扩展到它的所有子类声明的地方,
以及类和子类的所有方法的定义区(也就是方法的定义部分)。


TObject and TClass(TObject 和TClass)
类TObject 在System 单元声明,是所有其它类的最终祖先。TObject 只定义了少数方法,包括一个基本
的构造函数和析构函数。除了TObject,System 单元还声明了一个类引用类型TClass。
TClass = class of TObject;
如果在类声明中没有指定父类,则它直接继承于TObject,所以
type TMyClass = class
...
end;
等同于
type TMyClass = class(TObject)
...
end;
后者可读性较好,推荐使用。

Compatibility of class types(类类型兼容性)
类和它的祖先类是赋值兼容的,所以,某个类类型的变量能引用它的任何子类类型的实例。比如,在下
面的声明中
type
TFigure = class(TObject);
TRectangle = class(TFigure);
TSquare = class(TRectangle);
var
Fig: TFigure;
变量Fig 能被赋予TFigure、TRectangle 和TSquare 类型的值。


Object types(Object 类型)
除了类类型,你可以使用如下语法声明一个object 类型
type objectTypeName = object (ancestorObjectType)
memberList
end;
这里,objectTypeName 是任何有效标志符,(ancestorObjectType)是可选的,memberList 声明字段、方法
和属性。若(ancestorObjectType)被省略了,则新类型没有祖先。Object 类型不能有published 成员。
因为object 类型不是从TObject 继承,它们没有内置的构造函数和析构函数,也没有其它方法。你能使
用New 过程创建Object 类型的实例,并使用Dispose 过程销毁它们,你也可以像使用记录一样,采用简
单方式声明object 类型的变量。
Object 类型只是为了向后兼容性,不推荐使用它们。


Visibility of class members(类成员的可见性)
类的每个成员都有一个称为可见性的属性,我们用下面的关键字之一来表示它:private、protected、
public、published 和automated。比如
published property Color: TColor read GetColor write SetColor;
声明一个叫做Color 的published 属性。可见性决定了一个成员在哪些地方以及如何能被访问,private
表示最小程度的访问能力,protected 表示中等程度的访问能力,public、published 和automated 表示最
大程度的访问能力。
若声明一个成员时没有指定其可见性,则它和前面的成员拥有相同的可见性;若在类声明的开始没有指
定可见性,当在{$M+}状态下编译类时(或者继承自一个在{$M+}状态下编译的类),它的默认可见性是
published,否则,它的可见性是public。
为可读性考虑,最好在声明类时用可见性来组织成员:把所有的private 成员组织在一起,接下来是所有
的protected 成员,依此类推。用这种方法,每个可见性关键字最多出现一次,并且标明了每个新段的开
始。所以,一个典型的类声明应该像下面的形式:
type
TMyClass = class(TControl)
private
... { private declarations here}
protected
... { protected declarations here }
public
... { public declarations here }
published
... { published declarations here }
end;
通过重新声明,你可以在派生类中增大一个成员的可见性,但你不能降低它的可见性。比如,一个protected
属性在派生类中能被改变为public,但不能改为private。还有,published 成员在子类中不能改为public。
要了解更多信息,请参考Property overrides and redeclarations。
Private, protected, and public members(私有、受保护和公有成员)
Private 成员在声明它的单元或程序之外是不可用的,换句话说,一个private 方法不能从另一个模块
(module)进行调用,也不能从另一个模块读取或写入一个私有的字段或属性。通过把相关类的声明放
在一个模块中,可以使它们拥有访问其它类的私有成员的能力,同时又不会增大这些成员的访问范围。
Protected 成员在声明它的类的模块中是随处可用的,并且在它的派生类中也是可用的,而不管派生类出
现在哪个模块。换句话说,在派生类的所有方法定义中,你可以调用protected 方法,也能读取或写入
protected 字段或属性。只有在派生类的实现中才应用的成员通常使用protected 属性。
对于public 成员,只要能使用类的地方都是可用的。
Published members(公布的成员)
Published 成员和public 成员具有相同的可见性,不同之处是published 成员会产生RTTI 信息。RTTI
使应用程序能动态查询一个对象的字段和属性,也能定位它的方法。RTTI 用于在存储文件和从文件导入
时访问属性的值,也用于在Object Inspector 中显示属性,并且能为一些特定属性(叫做事件)关联特定
的方法(叫做事件处理程序)。
公布属性的数据类型受到限制,有序类型、字符串、类、接口和方法指针能被公布;当集合类型的基础
类型是有序类型,并且上界和下界介于0 到31 之间时(换句话说,集合必须符合byte、word 或double
word),集合类型也是可以公布的;除了Real48,任何实数类型都是可以公布的;数组类型的属性(区
别于数组属性,array properties)不能是公布的。
一些属性虽然是可以公布的,但不能完全支持流系统,它们包括:记录类型的属性、所有可公布类型的
数组属性以及包含匿名值的枚举类型的属性。如果published 属性属于前面所述的类型,Object Inspector
不能正确显示它们,并且使用流向磁盘操作时也不能保存它们的值。
所有方法都是可以公布的,但一个类不能使用相同的名字公布两个或以上数目的被重载的方法。只有当
字段属于类或接口类型时,它才是可以公布的。
A class cannot have published members unless it is compiled in the {$M+} state or descends from a class
compiled in the {$M+} state. Most classes with published members derive from TPersistent, which is compiled
in the {$M+} state, so it is seldom necessary to use the $M directive.
除非一个类是在{$M+}状态下被编译,或者派生于一个在{$M+}状态下被编译的类,否则它不能有公布
的成员。大多数具有公布成员的类继承自TPersistent,而它是在{$M+}状态下被编译的,所以通常很少
使用$M 编译器指示字。

Automated members(自动化成员)
Automated 成员和public 成员具有相同的可见性,不同之处是automated 成员会产生自动化类型信息
(Automation type information,自动化服务器需要)。Automated 成员只出现在Windows 类中,不推荐在
Linux 程序中使用。保留关键字automated 是为了向后兼容性,ComObj 单元的TAutoObject 类不使用自
动化成员。
对声明为automated 类型的方法和属性有以下限制:
• 所有属性、数组属性的参数、方法的参数以及函数的结果,它们的类型必须是自动化类型,包括Byte、
Currency、Real、Double、Longint、Integer、Single、Smallint、AnsiString、WideString、TDateTime、
Variant、OleVariant、WordBool 和所有接口类型。
• 方法声明必须使用默认的register 调用约定,它们可以是虚方法,但不能是动态方法。
• 属性声明可以包含访问限定符(读和写),但不能包含其它限定符(index、stored、default 和nodefault)。
访问限定符指定的方法必须使用默认的register 调用约定,并且限定符不能使用字段。
• 属性声明必须指定一个类型,并且属性不支持覆盖(override)。
Automated 方法或属性声明中可以包含dispid 指示字,但指定一个已经使用的ID 会导致错误。
在Windows 中,这个指示字的后面必须跟一个整数常数,它为成员指定一个Automation dispatch ID。否
则,编译器自动为它指定一个ID,这个ID 等于类(包括它的祖先类)的方法或属性使用的最大ID 加上
1。关于自动化的更多信息,请参考Automation objects。
Forward declarations and mutually dependent classes(Forward 声明
和相互依赖的类)
若声明一个类时以class 和分号结束,也就是有下面的格式,
type className = class;
在class 后面没有列出父类,也没有成员列表,这是一个forward 声明。Forward 声明的类必须在同一个
声明区域进行定义声明,换句话说,在forward 声明和它的定义声明之间除了类型声明外,不能有任何
其它内容。
Forward 声明允许创建相互依赖的类,比如
type
TFigure = class; // forward 声明
TDrawing = class
Figure: TFigure;
...
end;
TFigure = class // 定义声明
Drawing: TDrawing;
...
end;
不要把forward 声明和继承自TObject、不包含任何类成员的完整类声明混淆:
type
TFirstClass = class; // 这是forward 声明
TSecondClass = class // 这是一个完整的类声明
end; //
TThirdClass = class(TObject); // 这是一个完整的类声明
Fields(字段)
字段就像属于对象的一个变量,它可以是任何类型,包括类类型(也就是说,字段可以存储对象的引用)。
字段通常具有private 属性。
给类定义字段非常简单,就像声明变量一样。字段声明必须出现在属性声明和方法声明之前,比如,下
面的声明创建了一个叫做TNumber 的类,除了继承自TObject 的方法之外,它有一个唯一的整数类型的
成员Int。
type TNumber = class
Int: Integer;
end;
字段是静态绑定的,也就是说,它们的引用在编译时是固定的。要理解上面的意思,请考虑下面的代码:
type
TAncestor = class
Value: Integer;
end;
TDescendant = class(TAncestor)
Value: string; // 隐藏了继承的Value 字段
end;
var
MyObject: TAncestor;
begin
MyObject := TDescendant.Create;
MyObject.Value := 'Hello!'; // 错误
TDescendant(MyObject).Value := 'Hello!'; // 工作正常
end;
虽然MyObject 存储了TDescendant 的一个实例,但它是以TAncestor 声明的,所以,编译器把
MyObject.Value 解释为TAncestor 声明的整数字段。不过要知道,在TDescendant 对象中,这两个字段都
是存在的,继承下来的字段被新字段隐藏了,但可以通过类型转换对它进行操作。
Methods(方法)
Methods: Overview(概述)
方法是一个和类相关联的过程或函数,调用一个方法需指定它作用的对象(若是类方法,则指定类),比
如,
SomeObject.Free
调用SomeObject 的Free 方法。


Method declarations and implementations(方法声明和实现)
在类声明中,方法看起来像过程头和函数头,工作起来像forward 声明。在类声明后的某个地方(必须
属于同一模块),每个方法必须有一个定义声明来实现它。比如,假设TMyClass 类声明包含一个叫做
DoSomething 的方法:
type
TMyClass = class(TObject)
...
procedure DoSomething;
...
end;
DoSomething 的定义声明必须在模块的后面出现:
procedure TMyClass.DoSomething;
begin
...
end;
虽然类声明既可以出现在单元的interface 部分,也可以出现在implementation 部分,但类方法的实现(定
义声明)必须出现在implementation 部分。
在定义声明的头部,方法名总是使用类名进行限定。在方法的头部可以重新列出类声明时的参数,若这
样做的话,参数的顺序、类型以及名称必须完全相同,若方法是函数的话,返回值也必须相同。
方法声明可包含一些特殊指示字,而它们不会出现在其它函数或过程中。指示字应当只出现在类声明中,
并且以下面的顺序列出:
reintroduce; overload; binding; calling convention; abstract; warning
这里,

binding 是virtual、dynamic 或override;

calling convention 是register、pascal、cdecl、stdcall 或safecall;

warning 是platform、deprecated 或library。


Inherited(继承)
关键字inherited 在实现多态行为时扮演着特殊角色,它出现在方法定义中,后面跟一个标志符或者不跟。
若inherited 后面跟一个成员名称,它表示一个通常的方法调用,或者是引用一个属性或字段(except that
the search for the referenced member begins with the immediate ancestor of the enclosing method’s class)。比
如,当
inherited Create(...);
出现在方法定义中时,它调用继承的Create 方法。
When inherited has no identifier after it, it refers to the inherited method with the same name as the enclosing
method. In this case, inherited takes no explicit parameters, but passes to the inherited method the same
parameters with which the enclosing method was called. For example,
当inherited 后面没有标志符时,它指的是和当前方法同名的继承下来的方法。在这种情况下,inherited
没有明确指定参数,但把当前使用的参数传给继承下来的方法。比如,
inherited;
经常出现在构造函数的实现中,它把相同的参数传给继承下来的构造函数。


Self(Self 变量)
在实现方法时,标志符Self 引用方法所属的对象。比如,下面是Classes 单元中TCollection 的Add 方法
的实现:
function TCollection.Add: TCollectionItem;
begin
Result := FItemClass.Create(Self);
end;
Add 方法调用FItemClass 的Create 方法,而FItemClass 所属的类总是TCollectionItem 的派生类,
TCollectionItem.Create 有一个TCollection 类型的单一参数,所以,Add 把此时TCollection 的实例传给它,
这以下面的代码表示:
var MyCollection: TCollection;
...
MyCollection.Add // MyCollection 被传给TCollectionItem.Create 方法
Self 在很多方面都有用,比如,一个在类中声明的成员(标志符)可能在方法中被重新声明,这种情况
下,你可以使用Self.Identifier 来访问原来的成员。
关于类方法中的Self,请参考Class methods。


Method binding(方法绑定)
Method binding: Overview(概述)
方法分为静态方法(默认)、虚方法和动态方法。虚方法和动态方法能被覆盖,它们可是是抽象的。当某
个类类型的变量存储的是它的派生类时,它们的意义开始发挥作用,它们决定了调用方法时哪种实现被
执行。
Static methods(静态方法)
方法默认是静态的。当调用一个静态方法时,类或对象被声明的类型决定了哪种实现被执行(编译时决
定)。在下面的例子中,Draw 方法是静态的。
type
TFigure = class
procedure Draw;
end;
TRectangle = class(TFigure)
procedure Draw;
end;
给定上面的声明,下面的代码演示了静态方法执行时的结果。在第2 个Figure.Draw 中,变量Figure 引
用的是一个TRectangle 类型的对象,但却执行TFigure 中的Draw 方法,因为Figure 变量声明的类型是
TFigure。
var
Figure: TFigure;
Rectangle: TRectangle;
begin
Figure := TFigure.Create;
Figure.Draw; // 调用TFigure.Draw
Figure.Destroy;
Figure := TRectangle.Create;
Figure.Draw; // 调用TFigure.Draw
TRectangle(Figure).Draw; // 调用TRectangle.Draw
Figure.Destroy;
Rectangle := TRectangle.Create;
Rectangle.Draw; // 调用TRectangle.Draw
Rectangle.Destroy;
end;


Virtual and dynamic methods(虚方法和动态方法)
要实现虚方法或动态方法,在声明时包含virtual 或dynamic 指示字。不像静态方法,虚方法和动态方
法能在派生类中被覆盖。当调用一个被覆盖的方法时,类或对象的实际类型决定了哪种实现被调用(运
行时),而不是它们被声明的类型。
要覆盖一个方法,使用override 指示字重新声明它就可以了。声明被覆盖的方法时,它的参数的类型和
顺序以及返回值(若有的话)必须和祖先类相同。
在下面的例子中,TFigure 中声明的Draw 方法在它的两个派生类中被覆盖了。
type
TFigure = class
procedure Draw; virtual;
end;
TRectangle = class(TFigure)
procedure Draw; override;
end;
TEllipse = class(TFigure)
procedure Draw; override;
end;
给定上面的声明,下面代码演示了虚方法被调用时的结果,在运行时,执行方法的变量,它的实际类型
是变化的。
var
Figure: TFigure;
begin
Figure := TRectangle.Create;
Figure.Draw; // 调用TRectangle.Draw
Figure.Destroy;
Figure := TEllipse.Create;
Figure.Draw; // 调用TEllipse.Draw
Figure.Destroy;
end;
只有虚方法和动态方法能被覆盖,但是,所有方法都能被重载,请参考Overloading methods。

Virtual versus dynamic(比较虚方法和动态方法)
虚方法和动态方法在语义上是相同的,唯一的不同是在运行时决定方法调用的实现方式上,虚方法在速
度上进行了优化,而动态方法在代码大小上做了优化。
通常情况下,虚方法是实现多态行为的最有效的实现方式。当基类声明了大量的要被许多派生类继承的
(可覆盖的)方法、但只是偶尔才覆盖时,动态方法还是比较有用的。


Overriding versus hiding(比较覆盖和隐藏)
在声明方法时,如果它和继承的方法具有相同的名称和参数,但不包含override,则新方法仅仅是隐藏
了继承下来的方法,并没有覆盖它。这样,两个方法在派生类中都存在,方法名是静态绑定的。比如,
type
T1 = class(TObject)
procedure Act; virtual;
end;
T2 = class(T1)
procedure Act; // 重新声明Act,但没有覆盖
end;
var
SomeObject: T1;
begin
SomeObject := T2.Create;
SomeObject.Act; // 调用T1.Act
end;
Reintroduce(重新引入)
reintroduce 指示字告诉编译器,当隐藏一个先前声明的虚方法时,不给出警告信息。比如,
procedure DoSomething; reintroduce; // 父类也有一个DoSomething 方法
当要使用新方法隐藏继承下来的虚方法时,使用reintroduce 指示字。


Abstract methods(抽象方法)
抽象方法是虚方法或动态方法,并且在声明它的类中没有实现,而是由它的派生类来实现。声明抽象方
法时,必须在virtual 或dynamic 后面使用abstract 指示字。比如,
procedure DoSomething; virtual; abstract;
只有当抽象方法在一个类中被覆盖时,你才能使用这个类或它的实例进行调用。


Overloading methods(重载方法)
一个方法可以使用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!'); // 调用T2.Test
SomeObject.Test(7); // 调用T1.Test
在一个类中,你不能以相同的名字公布(published)多个重载的方法,维护RTTI 信息要求每一个公布
的成员具有不同的名字。
type
TSomeClass = class
published
function Func(P: Integer): Integer;
function Func(P: Boolean): Integer // 错误
...
作为属性读写限定符的方法不能被重载。
实现重载的方法时,必须重复列出类声明时方法的参数列表。关于重载的更多信息,请参考Overloading


procedures and functions。
Constructors(构造函数)
构造函数是一个特殊的方法,用来创建和初始化一个实例对象。声明一个构造函数就像声明一个过程,
但以constructor 开头。比如:
constructor Create;
constructor Create(AOwner: TComponent);
构造函数必须使用默认的register 调用约定。虽然声明中没有指定返回值,但构造函数返回它创建的对
象引用,或者对它进行调用的对象(的引用)。
一个类的构造函数可以不止一个,但大部分只有一个。按惯例,构造函数通常命名为Create。
要创建一个对象,在类(标志符)上调用构造函数。比如,
MyObject := TMyClass.Create;
它在堆中为对象分配内存,并设置所有的有序类型的字段为0,把nil 赋给所有的指针和类类型的字段,
使所有的字符串类型的字段为空;接下来,构造函数中指定的其它动作(命令)开始执行,通常,初始
化对象是基于传给构造函数的参数值;最后,构造函数返回新创建的对象的引用,此时它已完成了初始
化。返回值的类型与调用构造函数的类相同。
当使用类引用来调用构造函数时,若执行过程中发生了异常,则自动调用析构函数Destroy 来销毁不完
整的对象。
当使用对象引用来调用构造函数时(而不是使用类引用),它不是创建一个对象;取而代之的是,构造函
数作用在指定的对象上,它只是执行构造函数中的命令语句,然后返回一个对象的引用(是怎样的对象
引用,和调用它的一样吗?)。使用对象引用来调用构造函数时,通常和关键字inherited 一起使用来调
用一个继承的构造函数。
下面是一个类和构造函数的例子。
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); // 初始化继承下来的部分
Width := 65; // 改变继承下来的属性
Height := 65;
FPen := TPen.Create; // 初始化新字段
FPen.OnChange := PenChanged;
FBrush := TBrush.Create;
FBrush.OnChange := BrushChanged;
end;
构造函数的第一步,通常是调用继承下来的构造函数,对继承的字段进行初始化;然后对派生类中新引
入的字段进行初始化。因为构造函数总是把为新对象分配的内存进行“清零”(clear),所以,对象的所
有字段开始时都是0(有序类型)、nil(指针和类)、空(字符串)或者Unassigned(变体类型)。所以,
除非字段的值不为0 或者空值,我们没必要在构造函数中初始化各字段。
当使用类标志符调用构造函数时,声明为虚方法的构造函数和声明为静态时是相同的。但是,当和类引
用(class-reference)结合使用时,虚构造函数允许使用多态,也就是说,在编译时,对象的类型是未知
的(参考Class references)。


Destructors(析构函数)
析构函数是一个特殊的方法,用来销毁调用的对象并且释放它的内存。声明一个析构函数就像声明一个
过程,但以destructor 开头。比如:
destructor Destroy;
destructor Destroy; override;
析构函数必须使用默认的register 调用约定。虽然一个类的析构函数可以不止一个,但推荐每个类覆盖
继承下来的Destroy 方法,并不再声明其它析构函数。
要调用析构函数,必须使用一个实例对象的引用。比如,
MyObject.Destroy;
当析构函数被调用时,它里面的命令首先被执行,通常,这包括销毁所有的嵌入对象以及释放为对象分
配的资源;接下来,为对象分配的内存被清除。
下面是一个析构函数实现的例子:
destructor TShape.Destroy;
begin
FBrush.Free;
FPen.Free;
Classes and objects
- 107 -
inherited Destroy;
end;
析构函数的最后一步,通常是调用继承下来的析构函数,用来销毁继承的字段。
When an exception is raised during creation of an object, Destroy is automatically called to dispose of the
unfinished object. This means that Destroy must be prepared to dispose of partially constructed objects. Because
a constructor sets the fields of a new object to zero or empty values before performing other actions, class-type
and pointer-type fields in a partially constructed object are always nil. A destructor should therefore check for
nil values before operating on class-type or pointer-type fields. Calling the Free method (defined in TObject),
rather than Destroy, offers a convenient way of checking for nil values before destroying an object.
当创建对象时发生了异常,会自动调用析构函数来清除不完整的对象,这表示析构函数必须准备好来清
除只构建了一部分的对象。因为构造函数在执行其它动作之前先设置新对象的字段为0 或空值,在一个
只构建了一部分的对象中,类类型和指针类型的字段总是nil,所以,在操作类类型和指针类型的字段时,
析构函数必须检查它们是否为nil。销毁一个对象时调用Free 方法(在TObject 中定义)而不是Destroy
会更加方便,因为前者会检查对象是否为nil。


Message methods(Message 方法)
Message 方法用来响应动态分派的消息。Message 方法在各个平台上都是支持的,VCL 使用message 方
法来响应Windows 消息,CLX 不使用message 方法来响应系统事件。
在声明方法时,通过包含message 指示字来创建一个message 方法,并在message 后面跟一个介于1 到
49151 之间的整数常量,它指定消息的号码(ID)。对于VCL 控件(control),message 方法中的整数常
量可以是Messages 单元中定义的Windows 消息号码,这里还定义了相应的记录类型。一个message 方
法必须是具有一个单一var 参数的过程。
比如,在Windows 下:
type
TTextBox = class(TCustomControl)
private
procedure WMChar(var Message: TWMChar); message WM_CHAR;
...
end;
比如,在Linux 或在跨平台的情况下,你要以如下方式处理消息:
const
ID_REFRESH = $0001;
type
TTextBox = class(TCustomControl)
private
procedure Refresh(var Message: TMessageRecordType); message ID_REFRESH;
...
end;
Message 方法不必包含override 指示字来覆盖一个继承的message 方法。实际上,在覆盖方法时也不必
指定相同的方法名称和参数类型,而只要一个消息号码就决定了这个方法响应哪个消息和是否覆盖一个
方法。
Implementing message methods(实现message 方法)
The implementation of a message method can call the inherited message method, as in this example (for
Windows):
实现一个message 方法时,可以调用继承的message 方法,就像下面的例子(适用于Windows):
procedure TTextBox.WMChar(var Message: TWMChar);
begin
if Chr(Message.CharCode) = #13 then
ProcessEnter
else
inherited;
end;
在Linux 或跨平台的情况下,你要以如下方式实现同样的目的:
procedure TTextBox.Refresh(var Message: TMessageRecordType);
begin
if Chr(Message.Code) = #13 then
...
else
inherited;
end;
命令inherited 按类的层次结构向后寻找,它将调用和当前方法具有相同消息号码的第一个(message)
方法,并把消息记录(参数)自动传给它。如果没有祖先类实现message 方法来响应给定的消息号码,
inherited 调用TObject 的DefaultHandler 方法。
DefaultHandler 没有做任何事,只是简单地返回而已。通过覆盖DefaultHandler,一个类可以实现自己对
消息的响应。在Windows 下,VCL 控件(control)的DefaultHandler 方法调用Windows 的DefWindowProc
(API)函数。


Message dispatching(消息分派)
消息处理函数很少直接调用,相反,消息是通过继承自TObject 的Dispatch 方法来分派给对象的。
procedure Dispatch(var Message);
传给Dispatch 的参数Message 必须是一个记录,并且它的第一个字段是Cardinal 类型,用来存储消息号
码。
Dispatch 按类的层次结构向后搜索(从调用对象所属的类开始),它将调用和传给它的消息具有相同号码
的message 方法。若没有发现指定号码的message 方法,Dispatch 调用DefaultHandler。


Properties(属性)
Properties: Overview(概述)
属性就像一个字段,它定义了对象的一个特征。但字段仅仅是一个存储位置,它的内容可以被查看和修
改;而属性通过读取或修改它的值与特定的行为关联起来。属性通过操纵一个对象的特征来提供对它的
控制,它们还使特征能被计算。
声明属性时要指定名称和类型,并且至少包含一个访问限定符。属性声明的语法是
property propertyName[indexes]: type index integerConstant specifiers;
这里
• propertyName 是任何有效标志符;
• [indexes]是可选的,它是用分号隔开的参数声明序列,每个参数声明具有如下形式:identifier1, ...,
identifiern: type。更多信息请参考Array properties;
• type 必须是内置的或前面声明的数据类型,也就是说,像property Num: 0..9 ...这样的属性声明是非
法的;
• index integerConstant 子句是可选的。更多信息请参考Index specifiers;
• specifiers 是由read、write、stored、default(或nodefault)和implements 限定符组成的序列。每
个属性声明必须至少包含一个read 或write 限定符。
属性由它们的访问限定符定义。不像字段,属性不能作为var 参数传递,也不能使用@运算符,原因是
属性不一定(是不一定,还是一定不呢?)在内存中存在。比如,它可能有一个读方法从数据库中检索
一个值或者产生一个随机数值。

Property access(属性访问)
每个属性有一个读限定符,一个写限定符,或两者都有,它们称为访问限定符,具有以下的格式
read fieldOrMethod
write fieldOrMethod
这里,fieldOrMethod 是一个字段或方法名,它们既可以和属性在同一个类中声明,也可以在祖先类中声
明。
• 如果fieldOrMethod 和属性是在同一个类中声明的,它必须出现在属性声明的前面;如果它是在祖先
类中声明的,则它对派生类必须是可见的,也就是说,若祖先类在不同的单元声明,则fieldOrMethod
不能是私有的字段或方法;
• 若fieldOrMethod 是一个字段,它的类型和属性必须相同;
• 若fieldOrMethod 是一个方法,它不能是重载的,而且,对于公布的属性,访问方法必须使用默认的
register 调用约定;
• 在读限定符中,若fieldOrMethod 是一个方法,它必须是一个不带参数的函数,并且返回值和属性具
有相同的类型;
• 在写限定符中,若fieldOrMethod 是一个方法,它必须是一个带有单一值参(传值)或常量参数的过
程,这个参数和属性具有相同的类型;
比如,给定下面的声明
property Color: TColor read GetColor write SetColor;
GetColor 方法必须被声明为:
function GetColor: TColor;
SetColor 方法必须被声明为下面之一:
procedure SetColor(Value: TColor);
procedure SetColor(const Value: TColor);
(当然,SetColor 的参数名不必非得是Value。)
当在表达式中使用属性时,通过在读限定符中列出的字段或方法读取它的值;当在赋值语句中使用属性
时,通过写限定符列出的字段或方法对它进行写入。
在下面的例子中,我们声明了一个叫做TCompass 的类,它有一个公布的属性Heading。Heading 的值通
过FHeading 字段读取,写入时使用SetHeading 过程。
type
THeading = 0..359;
TCompass = class(TControl)
private
FHeading: THeading;
procedure SetHeading(Value: THeading);
published
property Heading: THeading read FHeading write SetHeading;
...
end;
给出上面的声明,语句
if Compass.Heading = 180 then GoingSouth;
Compass.Heading := 135;
对应于
if Compass.FHeading = 180 then GoingSouth;
Compass.SetHeading(135);
在TCompass 类中,读取Heading 属性时没有执行任何命令,只是取回存储在FHeading 字段的值;另一
方面,给Heading 赋值变成了对SetHeading 方法的调用,我们推测,它的操作将是把新值存储在FHeading
字段,还可能包括其它命令。比如,SetHeading 可能以如下方式实现:
procedure TCompass.SetHeading(Value: THeading);
begin
if FHeading <> Value then
begin
FHeading := Value;
Repaint; // 刷新用户界面来反映新值
end;
end;
若声明属性时只有读限定符,则它是只读属性;若只有写限定符,则它是只写属性。当给一个只读属性
赋值,或在表达式中使用只写属性时都将产生错误。


Array properties(数组属性)
数组属性是被索引的属性,它们能表示像下面的一些事物:列表中的条目、一个控件的子控件和位图中
的象素等等。
声明数组属性时包含一个参数列表,它指定索引的名称和类型,比如,
property Objects[Index: Integer]: TObject read GetObject write SetObject;
property Pixels[X, Y: Integer]: TColor read GetPixel write SetPixel;
property Values[const Name: string]: string read GetValue write SetValue;
索引参数列表的格式和过程(或函数)的参数列表相同,除了使用中括号取代了圆括号。不像数组只使
用有序类型的索引,数组属性的索引能使用任何类型。
对数组属性,访问限定符必须使用方法而不是字段。读限定符的方法必须是一个函数,它的参数数目、
类型以及顺序必须和索引中列出的一致,并且返回值和属性是同一类型;对写限定符,它必须是一个过
程,这个过程必须使用索引中列出的参数,包括数目、类型以及顺序必须相同,另外再加一个和属性具
有相同类型的值参(传值)或常量参数。
比如,前面的属性可能具有如下的访问方法声明:
function GetObject(Index: Integer): TObject;
function GetPixel(X, Y: Integer): TColor;
function GetValue(const Name: string): string;
procedure SetObject(Index: Integer; Value: TObject);
procedure SetPixel(X, Y: Integer; Value: TColor);
procedure SetValue(const Name, Value: string);
一个数组属性通过使用属性索引来进行访问。比如,语句
if Collection.Objects[0] = nil then Exit;
Canvas.Pixels[10, 20] := clRed;
Params.Values['PATH'] := 'C:\DELPHI\BIN';
对应于
if Collection.GetObject(0) = nil then Exit;
Canvas.SetPixel(10, 20, clRed);
Params.SetValue('PATH', 'C:\DELPHI\BIN');
在Linux 下,上面的例子你要使用像“/usr/local/bin”的路径取代“C:\DELPHI\BIN”。
定义数组属性时可以在后面使用default 指示字,此时,数组属性变成类的默认属性。比如,
type
TStringArray = class
public
property Strings[Index: Integer]: string ...; default;
...
end;
若一个类有默认属性,你能使用缩写词object[index]来访问这个属性,它就相当于object.property[index]。
比如,给定上面的声明,StringArray.Strings[7]可以缩写为StringArray[7]。一个类只能有一个默认属性,
在派生类中改变或隐藏默认属性可能导致无法预知的行为,因为编译器总是静态绑定一个对象地默认属
性。


Index specifiers(索引限定符)
索引限定符能使几个属性共用同一个访问方法来表示不同的值。索引限定符包含index 指示字,并在后
面跟一个介于-2147483647 到2147483647 之间的整数常量。若一个属性有索引限定符,它的读写限定符
必须是方法而不能是字段。比如,
type
TRectangle = class
private
FCoordinates: array[0..3] of Longint;
function GetCoordinate(Index: Integer): Longint;
procedure SetCoordinate(Index: Integer; Value: Longint);
public
property Left: Longint index 0 read GetCoordinate write SetCoordinate;
property Top: Longint index 1 read GetCoordinate write SetCoordinate;
property Right: Longint index 2 read GetCoordinate write SetCoordinate;
property Bottom: Longint index 3 read GetCoordinate write SetCoordinate;
property Coordinates[Index: Integer]: Longint read GetCoordinate write
SetCoordinate;
...
end;
对于有索引限定符的属性,它的访问方法必须有一个额外的整数类型的值参:对于读取函数,它必须是
最后一个参数;对于写入过程,它必须是倒数第2 个参数(在指定属性值的参数之前)。当程序访问属性
时,属性的整数常量自动传给访问方法。
给出上面的声明,若Rectangle 属于TRectangle 类型,则
Rectangle.Right := Rectangle.Left + 100;
对应于
Rectangle.SetCoordinate(2, Rectangle.GetCoordinate(0) + 100);

Storage specifiers(存储限定符)
可选指示字stored、default 和nodefault 被称为存储限定符,它们对程序的行为没有影响,但决定了RTTI
的维护方式,它们决定是否把公布属性的值存储到窗体文件中。
stored 指示字后面必须跟True、False、Boolean 类型的字段名或者一个返回Boolean 值的无参数方法。
比如,
property Name: TComponentName read FName write SetName stored False;
若一个属性没有stored 指示字,就相当于指定了stored True。
default 指示字后面必须跟随一个和属性具有相同类型的常量,比如,
property Tag: Longint read FTag write FTag default 0;
要覆盖一个继承下来的默认值而不指定新值,使用nodefault 指示字。default 和nodefault 只支持有序类
型和集合类型(当它的基础类型是有序类型,并且上下边界都在0 到31 之间时)。若声明属性时没有使
用default 或者nodefault,它被当作nodefault 看待。对于实数、指针和字符串,它们分别有隐含的默认
值0、nil 和 ' '(空串)
当保存一个组件的状态时,组件中公布属性的存储限定符会被检查,若属性的当前值和默认值不同(或
没有默认值),并且stored 为True,则它的值就会被保存;否则,属性的值不被保存。
注意:存储限定符不支持数组属性。在声明数组属性时,指示字default 有不同的意义。
Property overrides and redeclarations(属性的覆盖和重新声明)
声明时没有指定类型的属性称为属性覆盖,它允许你改变一个属性继承下来的可见性或限定符。最简单
的覆盖只包含关键字property、并在后面跟属性标志符,这种方式用来改变属性的可见性。比如,祖先
类声明了一个受保护的属性,派生类可以重新声明它为公有的或公布的。属性覆盖可包含read、write、
stored、default 和nodefault,它们覆盖了继承下来的相应指示字。覆盖可以取代访问限定符、添加限定
符或增大属性的可见性,但不能删除访问限定符或降低可见性。覆盖可包含implements 指示字,它添加
可以实现的接口,但不能删除继承下来的那些。
下面的声明演示了属性覆盖的使用:
type
TAncestor = 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(TAncestor)
...
protected
property Size write SetSize;
published
property Text;
property Color stored True default clBlue;
...
end;
覆盖的Size 属性添加了写限定符,允许属性能被修改;覆盖的Text 和Color 属性把可见性从protected
改变为published;覆盖的Color 属性还指定若它的值不为clBlue,它将被保存进文件。
若重新声明属性时包含类型标志符,这将隐藏继承下来的属性而不是覆盖它,也就是创建了一个(和继
承下来的属性)具有相同名称的新属性。任何指定类型的属性声明必须是完整的,也就至少要包含一个
访问限定符。
派生类中属性是隐藏还是覆盖呢?属性的查找总是静态的,也就是说,对象(变量)声明的类型决定了
它的属性。所以,在下面的代码执行后,读取MyObject.Value 或给它赋值将调用Method1 或Method2,
即使MyObject 存储的是TDescendant 的一个实例;但你可以把MyObject 转换为TDescendant 来访问派
生类的属性和它们的访问限定符。
type
TAncestor = class
...
property Value: Integer read Method1 write Method2;
end;
TDescendant = class(TAncestor)
...
property Value: Integer read Method3 write Method4;
end;
var MyObject: TAncestor;
...
MyObject := TDescendant.Create;


Class references(类引用)
Class references: Overview(概述)
有时,我们需要使用类本身而不是它的实例(也就是对象),比如,当使用类引用来调用构造函数时。你
总是能使用类名来引用一个类,但有时,你也需要声明变量或参数把类作为它的值,这种情况下,你需
要使用类引用类型。
Class-reference types(类引用类型)
类引用类型有时称为元类,用如下的构造形式表示
class of type
这里,type 是任何类类型。type(标志符)本身表示一个class of type(元类)类型的值。若type1 是type2
的祖先类,则class of type2(元类)和class of type1(元类)是赋值兼容的。这样
type TClass = class of TObject;
var AnyObj: TClass;
声明了一个叫做AnyObj 的变量,它能存储任何类引用。类引用类型的声明不能直接用于变量或参数声
明中。你能把nil 值赋给任何类引用变量。
要了解类引用类型如何使用,看一下TCollection(在Classes 单元)的构造函数声明:
type TCollectionItemClass = class of TCollectionItem;
...
constructor Create(ItemClass: TCollectionItemClass);
上面声明说,要创建一个TCollection 实例对象,你必须向构造函数传递一个类名,它属于TCollectionItem
类或是它的派生类。
当你调用一个类方法,或者调用一个类(或对象)的虚构造函数(编译时它们的类型不能确定)时,类
引用是很有用的。

类引用的用途就是创建在编译器无法确定的对象,举个列子:
Type
TControlCls = Class of TControl;
function CreateComponent(ControlCls: TControlCls): TControl;
begin
result:=ControlCls.Create(Form1);
...
end;

调用时如:
CreateComponent(TMemo);//创建TMemo对象
CreateComponent(TEdit);//创建TEdit对象


Constructors and class references(构造函数和类引用)
构造函数可通过一个类引用类型的变量进行调用,这允许创建编译时类型并不确定的对象。比如,
type TControlClass = class of TControl;
function CreateControl(ControlClass: TControlClass;
const ControlName: string; X, Y, W, H: Integer): TControl;
begin
Result := ControlClass.Create(MainForm);
with Result do
begin
Parent := MainForm;
Name := ControlName;
SetBounds(X, Y, W, H);
Visible := True;
end;
end;
CreateControl 函数需要一个类引用类型的参数,它指定创建何种控件,函数使用这个参数来调用构造函
数。因为类标志符(类名)表示一个类引用的值,所以能使用它作为参数来调用CreateControl 创建一个
实例。比如,
CreateControl(TEdit, 'Edit1', 10, 10, 100, 20);
使用类引用来调用的构造函数通常是虚方法,实际调用的构造函数(指实现)由运行时类引用的类型决定。


Class operators(类运算符)
Class operators: Overview(概述)
每个类从TObject 继承了两个分别叫做ClassType 和ClassParent 的方法,前者返回对象的类引用,后者
返回对象的父类类引用。这两个方法的返回值都是TClass(这里TClass = class of TObject)类型,它们
能被转换为更加明确的类型。每个类还继承了一个叫做InheritsFrom 的方法,它测试调用的对象是否从
一个指定的类派生而来(如果对象是类的一个实例,结果如何?)。这些方法被is 和as 运算符使用,很
少直接调用它们。


The is operator(is 运算符)
is 运算符执行动态类型检查,用来验证运行时一个对象的实际类型。
object is class
若object 对象是class 类的一个实例,或者是class 派生类的一个实例,上面的表达式返回True,否则返
回False(若object 是nil,则结果为False)。如果object 声明的类型和class 不相关,也就是说,若两个
类不同并且其中一个不是另一个的祖先,则发生编译错误。比如,
if ActiveControl is TEdit then TEdit(ActiveControl).SelectAll;
上面的语句先检查一个对象(变量)是否是TEdit 或它的派生类的一个实例,然后再决定是否把它转换
为TEdit。


The as operator(as 运算符)
as 运算符执行受检查的类型转换。表达式
object as class
返回和object 相同的对象引用,但它的类类型是class。在运行时,object 对象必须是class 类的一个实例,
或者是它的派生类的一个实例,或者是nil,否则将产生异常;若object 声明的类型和class 不相关,也
就是说,若两个类不同并且其中一个不是另一个的祖先,则发生编译错误。比如,
with Sender as TButton do
begin
Caption := '&Ok';
OnClick := OkClick;
end;
因为运算符优先权的问题,我们经常需要把as 类型转换放在一对括号中,比如,
(Sender as TButton).Caption := '&Ok';


Class methods(类方法)
类方法是作用在类而不是对象上面的方法(不同于构造函数)。类方法的定义必须以关键字class 开始,
比如,
type
TFigure = class
public
class function Supports(Operation: string): Boolean; virtual;
class procedure GetInfo(var Info: TFigureInfo); virtual;
...
end;
类方法的定义部分也必须以class 开始,比如,
class procedure TFigure.GetInfo(var Info: TFigureInfo);
begin
...
end;
在类方法的定义部分,Self 表示调用方法的类(which could be a descendant of the class in which it is
defined,它或许是定义方法的类的一个派生类)。若使用类C 调用方法,Self 的类型是class of C(元类)。
所以,你不能使用Self 访问字段、属性和平常的方法(由对象调用的方法),但能调用构造函数和其它
类方法。
类方法既可以通过类引用来调用,也可以使用对象,当使用后者时, Self 值等于对象所属的类。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics