类和类成员概述

                            类和类成员概述
类(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节,是因为方法重载更加复杂,在这里可以更加全面地来阐述它。


                                

查看回复