类和类成员概述
类(Class),是一个包含字段(Field,也称为域)、方法(Method)和属性(Property)(事件(Event)是一种特殊的属性)三种成员的构造体。
因为本书是讲"Delphi精要",所以对于面向对象理论中的类的概念,就不再使用什么"禽兽 | 家禽 | 鸡鸭鹅"之类例子来讲解了,如果大家对类的基本概念还不是很理解,那么可以参阅相关资料和书籍...
类和类成员概述
类(Class),是一个包含字段(Field,也称为域)、方法(Method)和属性(Property)(事件(Event)是一种特殊的属性)三种成员的构造体。
因为本书是讲"Delphi精要",所以对于面向对象理论中的类的概念,就不再使用什么"禽兽 | 家禽 | 鸡鸭鹅"之类例子来讲解了,如果大家对类的基本概念还不是很理解,那么可以参阅相关资料和书籍。
对象即类的实例,是使用构造函数(在Object Pascal中是用关键字constructors标识的,它是一个特殊的类方法,通常是Create)来生成的一个内存块。销毁一个对象使用析构函数(用关键字destructors标识,通常是Destroy)。
字段,就是在对象中对应某项数据的变量。有的资料和书籍也将字段称为域,在本书中,为了便于理解,一般都称作字段。
而方法则是一些函数和过程。方法可以分为普通方法和类方法两种,分别用来操作对象和类。普通方法只有由类实例调用,而类方法可以由类或者类实例调用。
属性,实际上是一些需要特殊处理的字段的包装,它们的值可以用字段或者方法来存取。
以下是摘自Forms单元的一段代码,这段代码声明了一个TCustomForm类(它是TForm的父类):
{TCustomForm派生于另一个类TScrollingWinControl,它们构成父子关系}
TCustomForm = class(TScrollingWinControl)
private
{声明一个字段FWindowState}
FWindowState: TWindowState;
{再声明一个字段FOnDestroy}
FOnDestroy: TNotifyEvent;
......
{声明一个方法SetWindowState}
procedure SetWindowState(Value: TWindowState);
......
public
{这是一个构造函数:Create}
constructor Create(AOwner: TComponent); override;
{这是一个析构函数:Destroy}
destructor Destroy; override;
{声明一个属性WindowState,它从字段FWindowState读取值,用方法SetWindowState
保存值(方法SetWindowState在内部将值保存到字段FWindowState)}
property WindowState: TWindowState read FWindowState write SetWindowState;
{声明一个特殊的属性——事件OnDestroy,和WindowState不同,OnDestroy的存取都是
通过字段FOnDestroy进行的}
property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy
......
end;
在本小节最后,我们渴望搞清楚类成员的可见性问题。对于类成员的其他深入知识,
我会开辟专门的小节来阐述。
类成员的可见性是对该类的使用者而言。在声明一个类时,类可以被分为5个区域,用以下5个关键字标识:
private, protected, public, published, automated。
所有的类成员都被放置在不同的区域里,不同区域的类成员具有不同的可见性。如果类的定义和类的使用者在同一个单元内,那么该类的所有成员无论位于哪个区域,对于使用者而言都是可见的。一个类对于相同单元的其他类来说,类似于C++中的"友类",其所有成员都可以被访问。因此,类成员的可见性设置只是在它们位于不同单元时,才是有效的。这时候,区域内成员的可见性规定如下:
(1) private域:总不可见。这个区域用来隐藏一些实现细节并防止使用者直接修改敏感信息,比如容纳属性的存取字段和方法。
(2) protected:派生类可见。这样既可以起到private域的作用,也能给派生类提供较大的灵活性。该区域常被用来定义虚方法。
(3) public:总可见。通常用来放置构造、析构函数等供使用者调用的方法。
(4) published:总可见。而且这个区域的类成员有运行时类型信息,该区域通常用来放置供使用者访问的属性和事件。
(5) automated:总可见。而且该域的成员具有自动化类型信息,用于创建自动化服务器。该关键字已经不再使用,为向后兼容保留。
类的成员通常都是很明确指定了它所属区域的,但并不总是这样,凡事都是有例外的。比如我们在窗体上放置一个按钮并双击它生成OnClick事件过程后,单元的源代码中对窗体类的定义就变成了下面的样子:
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
我们发现字段Button1和过程Button1Click并没有被明确地放到哪个可见性区域中。那么这时候它们的可见性按什么规则来确定呢?此时和编译指令$M密切相关。
后面我们要讲:$M用来控制编译器是否给类生成运行时类型信息。所以,在{$M+}状态,Button1和Button1Click被隐含指定到published域;在{$M-}状态,则到public域。
那么对于上面的TForm1来说,因为它现在处在{$M+}状态,所以Button1和Button1Click实际上被隐含指定到published域。
深入认识方法
在上一小节,我们已经简单说明了类的成员之一——方法。但是,对于方法仅仅只这么一点认识是远远不够的。
在本小节里,我打算从不同角度对方法分类研究,借以深入认识方法。
(1) 从调用者角度可分为:
① 普通方法;
② 类方法。
普通方法只能被类实例(即对象)调用,而类方法不但可以被对象调用,还可
以直接被类调用(比如构造函数Create和析构函数Destroy)。我们看下面的例子:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TOneObject = class
{声明一个类方法ClassProc。方法是在最前面加"Class"关键字}
class procedure ClassProc;
{声明一个普通方法OneProc}
procedure OneProc;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
class procedure TOneObject.ClassProc;
begin
ShowMessage('ClassProc');
end;
procedure TOneObject.OneProc;
begin
ShowMessage('OneProc');
end;
procedure TForm1.Button1Click(Sender: TObject);
var
OneObj: TOneObject;
begin
TOneObject.ClassProc; {类方法可以直接被类调用}
OneObj := TOneObject.Create; {Create本身也是个类方法}
OneObj. ClassProc; {对象也可以调用类方法}
OneObj.OneProc; {普通方法只能被对象调用}
OneObj.Free;
end;
end.
类方法是从C++的static(静态)函数借鉴而来的。实现一个类方法时,要特别注意不要让它依赖于任何实例信息,千万不要在类方法中存取字段、属性和普通方法。否则通过类而不是对象来调用它时,将发生错误,因为此时并没有实例信息。
在本小节接下来的内容里,我们不再讨论类方法,所有的方法都是指普通方法。
(2) 从调用机制上分:
① 静态方法。如下面的代码定义了TOneObject的静态方法OneProc。
TOneObject = class
procedure OneProc;
end;
没有修饰字的方法被默认为静态方法。和下面要讲的虚方法相比,静态方法能够获得更快的运行速度,因为它的地址是编译时确定、运行时映射的;而虚方法为了实现某些更加高级、灵活、复杂的功能,需要在运行时作一些附加处理(比如动态寻址),所以调用时相对要慢一些。
虚方法。虚方法使用关键字virtual或者dynamic声明,如:
TOneObject = class
procedure OneProc; virtual;
function OneFun: Boolean; dynamic;
end;
其中OneProc和OneFun都是虚方法。虚方法可以在子类中进行覆盖,从而增强方法的功能。覆盖一个虚方法应该使用override关键字。例如我们定义TOneObject的子类:
在子类中,可以(但不是必须的)覆盖父类的虚方法,从而实现更加复杂的控制。覆盖采用关键字override:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender
: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
{TParent声明了两个虚方法}
TParent = class
procedure OneProc; virtual;
function OneFun: Boolean; dynamic;
end;
{TChild派生于TParent,并对父类的两个虚拟方法都做了覆盖}
TChild = class(TParent)
procedure OneProc; override;
function OneFun: Boolean; override;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TParent }
procedure TParent.OneProc;
begin
ShowMessage('TParent');
end;
function TParent.OneFun: Boolean;
begin
Result := False;
end;
{ TChild }
procedure TChild.OneProc;
begin
inherited; {inherited调用父类的OneProc的代码,这句的结果是显示'TParent'}
ShowMessage('TChild');
end;
function TChild.OneFun: Boolean;
begin
Result := inherited OneFun; {调用父类的OneFun代码}
if not Result then
Result := TRue;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Child: TChild;
begin
Child := TChild.Create;
Child.OneProc; {会先显示'TParent'(父类代码实现的),再显示'TChild'
(子类实现的)}
if Child.OneFun then {条件语句成立}
ShowMessage('OneFun return true');
Child.Free;
end;
end.
使用不同关键字声明的虚方法是有区别的。virtual声明的称为虚拟方法,dynamic声明的称为动态方法。它们在被调用时,派遣机制有所不同,具体可以参看第5章的"虚拟方法表和动态方法表"小节。
因为声明虚方法的目的都是供子类覆盖,所以虚方法一般应该声明在protected区域,当然不是绝对的。如果希望类的使用者能够调用这个虚拟方法,那么还可以声明在public域。
还得对inherited作点说明。inherited并不仅仅局限在子类覆盖后的虚方法中调用父类中被覆盖的方法,实际上,inherited可以使用在任何地方、调用子类可见的任何父类方法(包括protected、public、published等域的)。
③ 抽象方法。它是虚方法的特例,在虚方法声明后加上abstract关键字构成,如:
TParent = class
procedure OneProc; virtual; abstract;
function OneFun: Boolean; dynamic; abstract;
end;
抽象方法和普通虚方法的区别:
a. 抽象方法只有声明,没有实现;而虚方法必须有实现部分,哪怕没有实际代码而只有begin...end头。
b. 抽象方法必须在子类中覆盖并实现后才用调用。因为没有实现的方法不能被分配实际地址,而调用一个没有实际地址的方法显然是荒谬的。
所以,抽象方法也可以被称为纯虚方法。
如果一个类中含有抽象方法,那么这个类就成了抽象类,如TStrings含有:
procedure Clear; virtual; abstract;
procedure Delete(Index: Integer); virtual; abstract;
等多个抽象
方法。
抽象类是不应该直接用来创建实例的,因为一旦调用了抽象方法,将抛出地址异常,而我们很难保证不在某些地方调用抽象方法。所以,尽管实例化抽象类是被允许的,却是应该避免的。
因此,抽象类一般都是中间类,实际使用的总是覆盖实现了抽象方法的子类。比如常用的字符串列表类TStrings,我们总是使用它的子类而不是它本身来构造实例,如:
var
Strs: TStrings;
begin
Strs := TStringList.Create;
......
end;
(3) 从用途来分:
① 重载方法。方法名相同,但参数个数或者类型不同的多个方法构成重载;重载的目的是得到多个同名但是功能不同的方法。重载是用关键字overload来指明的,比如:
TParent = class
procedure OneProc; overload;
function OneProc(S: String): Boolean; overload;
end;
上面TParent类的方法OneProc被重载。
重载方法的几个特点:
a. 可以分别是函数或者过程。因为在Delphi中,可以将过程看做一个没有返回值的函数,一个函数也可以当作过程调用。
b. 如果位于相同类中,都必须加上overload关键字;如果分别在父类和子类中,那么父类的方法可不加overload而子类必须加overload。
c. 如果父类的方法是虚(virtual或者dynamic)的,那么在子类中重载该方法时应该加上reintroduce修饰字,否则会出现编译警告:"hides virtual method of base type"。当然只是编译时产生警告,如果你不顾它的警告,坚持不加修饰字,对程序运行结果也不会造成影响。如:
TParent = class
procedure OneProc; virtual;
end;
TChild = class(TParent)
procedure OneProc; reintroduce; overload;
end;
d. 在published区不能出现多个相同的重载方法。如:
TParent = class
procedure OneProc; virtual;
end;
TChild = class(TParent)
published
procedure OneProc; reintroduce; overload;
{和父类构成方法重载关系是可以的,因为在TChild的published区,只有一个OneProc方
法,而下面两行企图重载AnotherProc则是没可能的,编译器不允许在published区出现
多个同名的方法}
procedure AnotherProc(S: String); overload;
procedure AnotherProc; overload;
end;
为什么编译器不允许在published出现多个同名的方法呢?别忘了在前面我们说过:published区的类成员会生成运行时类型信息的,而类成员是通过名字区分的。因此,这时候编译器无法为成员AnotherProc生成运行时类型信息。
程序运行时,能够根据传入的参数来正确区分应该调用哪一个被重载的方法。
重载的概念对于普通的过程和函数也是适用的,实际上方法重载是从普通过程和函数的重载引申而来的。我之所以没有将重载内容放在3.2节,是因为方法重
载更加复杂,在这里可以更加全面地来阐述它。