Object Pascal 面向对象的特性

2 面向对象的特性

  在软件系统开发过程中,结构分析技术和结构设计技术具有很多优点,但同时也存在着许多难以克服的缺点。因为结构分析技术和结构设计技术是围绕着实现处理功能来构造系统的,而在系统维护和软件升级的过程中,用户的需求变化往往是针对于系统功能的,所以采用这种技术设计的系统是不稳定的,其可修改性和可重用性是不完善的。在这种情况下,面向对象的程序设计技术产生了,它尽可能地模拟人类习惯的思维方式,使开发软件的方法和过程尽可能地接近人类认识世界、解决问题的方法与过程。采用面向对象的程序分析与设计技术开发的软件系统,其稳定性、可重用性和可维护性都很好。


  Delphi 是面向对象编程的先进开发环境。面向对象的程序设计(OOP)是结构化语言的自然延伸。OOP 的先进编程方法,会产生一个清晰而又容易扩展及维护的程序。一旦为用户的程序建立了一个对象,那么用户和程序开发人员可以在其他的程序中使用这个对象,完全不必重新编制繁复的代码。对象的重复使用可以大大地节省开发时间,切实地提高用户的工作效率。OOP 是使用独立对象(包含数据和代码)作为应用程序模块的范例。虽然OOP 不能使得代码容易编写,但是它能使代码易于维护。将数据和代码结合在一起,能够使定位和修复错误的工作变得简化,并最大限度地减少了对其他对象的影响,提高了程序的性能。

 

2.1 类和对象

  类是用户定义的一种数据类型,它有自己的内部数据、函数或过程的方法,用来描述一些相似的对象所拥有的共同特性和行为。对象是类的实例,它是由类定义的数据类型的变量。对象是实体,它与类之间是一种变量与变量类型的关系。

 

2.2 类的定义

  在Object Pascal 语言中,类和记录比较相似,也是一个构造类型,并且由属性和方法构成。其中属性又包括类的内部属性和外部属性,也就是供内部使用的一些数据变量和供外部使用的一些数据变量;方法则是该类或其实例可以操作的过程和函数。通常把类的内部属性称为字段,把字段、属性和方法统称为类的成员。

类的定义形式如下:

type ClassName = class (AncestorClass)
    MemberList
end;

上面的 ClassName 为类的名称,通常是一个以 T 开头的标识符。AncestorClass 是所继承的父类的名称。MemberList 为成员列表,可以声明一些变量和对象,也可以声明一些过程与函数。在Delphi 中,如果不指明父类,则默认的父类为 TObject 类,也就是直接从TObject 类派生出一个新类。TObject 类是在System 单元中定义的。类的所有成员都有一个标明“能见度”的属性,它们是由保留字Private、Protected、Public、Published或Automated 来说明的。通过这些保留字,可以控制对类中成员的访问权限。每个保留字的具体含义如下。

1.Private 属性
  具有 Private 属性的成员称为私有成员,不能被类所在单元以外的程序访问。也就是说,一个私有的属性不可以在所在模块之外的其他模块中读写,一个私有的方法也不可以在所在模块之外的其他模块中被调用。但是如果在同一个单元文件中定义了两个类(通常把关系非常紧密的两个类定义在同一个单元文件中),那么在一个类的成员中就可以对另一个类中的私有变量进行访问,或者调用另一个类中的私有方法。

2.Protected 属性
  具有 Protected 属性的成员称为保护成员,可以被该类的所有派生类访问,并且成为派生类的私有成员。

3.Public 属性
  具有 Public 属性的成员称为公有成员,可以被该类以外的类访问。如果两个类不在同一个单元文件中,则要在 Uses 语句中包括被访问类所在的单元名称。

4.Published 属性
  具有 Published 属性的成员称为发行类型成员,它的访问权限基本与公有成员相同,在设计期间也可以被访问。通常发行类型的成员用在组件类的声明中,这样,就可以在对象编辑器中访问组件的发行类型的成员了。

5.Automated 属性
  具有 Automated 属性的成员称为自动类型成员,它的访问权限基本也与公有成员相同。这种类型的成员一般用在从 TAutoObject 类派生的类中,目前只是为了和以前版本的Delphi 保持兼容才保留了 Automated 属性。除了在类封装的时候可以限制成员的访问权限外,在后面介绍的单元文件中也可以限制对变量、对象、函数和过程等的访问权限。为了使软件系统具有良好的安全性、健壮性,应该注意这些限制权限的用法。

 

2.3 类的封装

  一个类中会有多个数据成员和方法,而对于一个比较好的面向对象的程序设计,类的数据应当被封装,只能在类中使用它。封装的关键性规则是本模块代码的改变,不是对其他模块有负影响,这里的目标是创建需要时即可调用的模块(对象),而不需要特殊要求或负影响。例如不希望每次打开电视机的同时洗衣机也开始运行。

  封装的两个方面扮演着重要角色:

  内聚力是指模块的“单一性”,即每个成员函数应该做一件事,并且只做一件事。这个力度有助于创建可重用代码。如果一个程序执行 3 个不同的操作,将没有 3 个程序单独执行一个操作更具有可重用性。当重复使用代码时,不大可能以相同的顺序执行 3 个完全相同的操作。如果有小而内聚的程序,将发现重复使用它们更容易,因此应该尽量使程序内聚。

  耦合是封装的另一个重要方面。例如一个模块可能需要在另一个模块声明一个全局变量。这样大大减小了每个模块的可重用性。因为它们相互依赖,不能在另外一个不存在的情况下使用这个。一个程序必须以某种方式与应用程序耦合。所以最好是保持单一耦合点,常常是程序接受或返回的参数,通过提供单一耦合点,很容易抽出一个模块,使用相同耦合的另一个模块替换它。

 

2.4 类的继承

继承是指一个对象可以从另一个对象中继承一般特性的能力,然后再添加一些特定的功能。它可以重用常用的代码,因此减少了代码的编写。例如当建立一个新的窗体时,Delphi 会自动产生代码如下:

type
    TForm1=Class(TForm)
end;

 

2.5 构造与析构

在完成了类的封装之后,就可以使用这个类了。具体的步骤如下:

  • 声明类的一个变量----这时可以将类作为一种数据类型来看待。
  • 调用类的一个特殊函数----构造函数来进行一些初始化工作,比如按照类的结构来分配内存资源,完成对象的创建。
  • 对类的实例----对象进行操作、使用。可以修改对象的属性或调用对象的方法。
  • 使用完毕,调用类的另一个特殊函数----析构函数,删除创建的对象,同时释放相应的内存资源等。此外,还可以调用Free 过程释放对象占用的资源。

  构造函数和析构函数是类定义中两个非常重要的函数,它们完成的功能正好相反,定义也比较特殊。在声明了类的一个变量后,并没有实际创建该类的对象,只是定义了一个指向该类对象的指针,有时也称之为类的引用。对象的创建和初始化工作是由类的构造函数来完成的。

  在类的构造函数中,不仅可以根据类的结构为类的对象分配内存空间,而且还可以打开文件或数据库,读取一些初始数据,或者控制一些设备进行复位等。在定义构造函数的时候,不是使用保留字Function,而是使用保留字Constructor,通常函数名使用Create。如果在定义类的时候没有定义构造函数,则系统会自动为该类生成一个默认的构造函数。构造函数必须使用默认的函数调用约定方式,也就是使用 Register 指令字方式。程序员可以自定义一个或多个构造函数。自定义的构造函数可以有参数列表,可以重载构造函数。一般在自定义的构造函数的函数体中,在开始部分使用 inherited 保留字来调用父类的构造函数。如果在创建并初始化对象时,调用构造函数发生错误,则系统会自动调用析构函数来删除这个没有完成的对象。

  析构函数的作用是将对象删除并释放相应的内存资源,此外还可以在这之前保存一些数据信息并关闭文件或数据库等,或者对一些设备进行复位并关机。在定义析构函数的时候,使用保留字 Destructor 代替通常函数的 Function,函数名为 Destroy。如果在定义类的时候没有定义析构函数,则系统会自动为该类生成一个默认的析构函数。析构函数也必须使用默认的函数调用约定方式,也就是使用Register指令字方式。程序员也可以自定义析构函数。通常在自定义的析构函数的函数体中,在结尾部分使用inherited保留字来调用父类的析构函数。在释放对象占用的资源时也可以使用TObject 类的成员Free 过程。使用Free 过程可以删除一个对象,如果该对象不为nil,则会自动调用析构函数。通常在运行时创建的对象应该调用Free 过程来代替析构函数。如果对象没有被初始化,调用析构函数时就会出错,而调用Free 过程就没有问题。


下面的例子说明了类的定义和使用方法:

program Project1;
{$APPTYPE CONSOLE}

type
    TPerson = class //人员类
    public
        Name:string; //姓名
        function GetAge:Integer; //获取年龄
        procedure SetAge(A:Integer); //设置年龄
    private
        Age:Integer; //年龄
    end;

    TEmployee = class(TPerson) //职员类
    public
        Salary:integer; //薪金
        DeptName:string; //部门名称
        procedure Infor; //显示职员信息
        procedure SetSalary(A:Integer); //设置薪水
    end;

    TCustomer = class(TPerson) //顾客类
    public
        AddressName: string; //地址名称
        procedure Infor; //显示顾客信息
        constructor Create(Str: string);
        destructor Destroy;override;
    end;

function TPerson.GetAge:Integer; //获取人员的年龄
begin
    Result:=Age;
end;

procedure TPerson.SetAge(A:Integer); //设置人员的年龄
begin
    Age:=A;
end;

procedure TEmployee.SetSalary(A:Integer); //设置职员薪水
begin
    Salary:=A;
end;

procedure TEmployee.Infor; //显示职员的信息
begin
    Writeln('姓名:',Name,' 年龄:',GetAge,' 部门:',DeptName,' 薪水:',Salary) ;
end;

procedure TCustomer.Infor; //显示顾客的信息
begin
    Writeln('姓名:',Name,' 年龄:',GetAge,' 住址:' ,AddressName) ;
end;

constructor TCustomer.Create(Str: string); //顾客类的构造函数
begin
    inherited Create; //调用父类的构造函数
    Writeln('顾客类的构造函数。');
    AddressName:=Str;
end;

destructor TCustomer.Destroy; //顾客类的析构函数
begin
    Writeln ('姓名为:',Name,' 的顾客类对象被删除。' ) ;
    inherited Destroy; //调用父类的析构函数
end;

var
    E1: TEmployee; //声明一个职员类的变量
    C1: TCustomer; //声明一个顾客类的变量

begin
    E1:=TEmployee.Create; //调用默认的构造函数创建对象
    E1.Name:='李兵';
    E1.SetAge(23);
    E1.SetSalary(2000);
    E1.DeptName:='贷款部';
    E1.Infor; //显示职员信息
    E1.Destroy; //调用默认的析构函数

    C1:=TCustomer.Create('北一路'); //调用构造函数创建对象
    C1.Name:='张力';
    C1.Age:=45;
    // 显示顾客信息
    C1.Infor; //显示顾客信息
    C1.Destroy; //调用自定义的析构函数
end.

运行结果如下:

姓名:李兵 年龄:23 部门:贷款部 薪水:2000
调用顾客类的构造函数。
姓名:张力 年龄:45 住址:北一路
姓名为:张力 的顾客类对象被删除。

有几点需要说明:

  • 例程中的TPerson 类默认从TObject 类派生而来,然后它又派生出两个子类:TEmployee 类和TCustomer 类。TEmployee 类和TCustomer 类都继承了TPerson 类的Age 属性、Name 属性、GetAge函数和SetAge 过程等。
  • TEmployee 类对TPerson 类进行了功能扩展,又增加了DeptName,Salary 属性和Infor,SetSalary过程。
  • TCustomer 类对TPerson 类进行了功能扩展,增加了AddressName 属性和Infor 过程。此外,TCustomer 类中自定义了构造函数和析构函数。需要注意的是,应该在自定义的构造函数和析构函数的函数体中,调用父类的构造函数和析构函数的位置。
  • 调用构造函数的方法与调用析构函数的方法不同,前者使用的是“类的构造函数”,后者使用的是“对象的析构函数”。

  上面例子中的人员类可以看作是一个抽象类。定义抽象方法时只要用指令字Abstract 对其进行说明就可以了。在Delphi 中,抽象方法一定是虚拟方法或动态方法,所以,要在指令字Abstract 前用Virtual 或Dynamic 进行说明。此外,对于抽象方法不可以定义函数体。


下面来看一个抽象类的例子,相应代码如下:

program Project1;
{$APPTYPE CONSOLE}

type
    TFruit = class //水果类
        procedure Infor;virtual;abstract;
    end;

    TApple = class(TFruit) //苹果类
        Name:string;
        procedure Infor;override;
    end;

    TOrange = class(TFruit) //桔子类
        procedure Infor;override;
    end;

procedure TApple.Infor;
begin
    Writeln('苹果。');
end;

procedure TOrange.Infor ;
begin
    Writeln('桔子。');
end;

var
    A1: TApple; //声明一个苹果类的变量
    O1: TOrange; //声明一个桔子类的变量

begin
    A1:= TApple.Create;
    A1.Infor; //调用的是TApple 类的Infor
    A1.Destroy;

    O1:=TOrange.Create;
    O1.Infor; //调用的是TOrange 类的Infor
    O1.Destroy;
end.

运行结果如下:

苹果。
桔子。

 

2.6 方法

  方法是属于一个给定对象的过程和函数,方法反映的是对象的行为而不是数据,刚才提到了对象的两个重要的方法即构造器和析构方法。为了使对象能执行各种功能,所以能在对象中定制方法。


1.方法的类型
  对象的方法能定义成静态(Static)、虚拟(Virtual)、动态(Dynamic)或消息处理(Message)。


请看下面的例子:

Tfoo=class
    Procedure IamAStatic;
    Procedure IamAVitual;virtual;
    Procedure IamADynamic;dynamic;
    Procedure IamAMessage(var M:TMessage);message wm_SomeMessage;
end;

 

(1)静态方法
IAmAStatic 是一个静态方法。静态方法是方法的默认类型,它可以像通常的过程和函数那样被调用。编译器知道这些方法的地址,所以调用一个静态方法时它能把运行信息静态地链接到可执行文件。静态方法执行的速度最快,但它们却不能被覆盖来支持多态性。

(2)虚拟方法
IAmAVirtual 是一个虚拟方法。虚拟方法和静态方法的调用方式相同。由于虚拟方法能被覆盖,在代码中调用一个指定的虚拟方法时编译器并不知道它的地址,因此编译器需要通过建立虚拟方法表(VMT)来查找在运行时的函数地址。所有的虚拟方法在运行时通过VMT 来调度,一个对象的VMT表中除了自己定义的虚拟方法外,还有它的祖先的所有虚拟方法,因此虚拟方法占用的内存要比较多,但它执行得更快。


(3)动态方法
IAmADynamic 是一个动态方法。动态方法跟虚拟方法基本相似,只是它们的调度系统不同。编译器为每一个动态方法指定一个独一无二的数字,用这个数字和动态方法的地址构造一个动态方法表(DMT)。不像VMT 表,在DMT 表中仅有它声明的动态方法,并且这个方法需要祖先的DMT表来访问它其余的动态方法。正因为这样,动态方法比虚拟方法用的内存要少,但执行起来较慢,因为有可能要到祖先对象的DMT 中查找动态方法。


(4)消息处理方法
IAmAMessage 是一个消息处理方法。在关键字Message 后面的值指明了这个方法要响应的消息。用消息处理方法来响应Windows 的消息,这样就不用直接来调用它。


(5)方法的覆盖
在Object Pascal 覆盖一个方法用来实现OOP 的多态性概念。通过覆盖,使这一方法在不同的派生类间表现出不同的行为。Object Pascal 中能被覆盖的方法是在声明时被标识为Virtual 或Dynamic 的方法。为了覆盖一个方法,在派生类的声明中用Override 代替Virtual 或Dynamic。


例如用下面的代码覆盖IAmAVirtual 和IAmADynamic 方法:

TfooChild=class(TFoo)
    Procedure IamAVitual;override;
    Procedure IamADynamic;override;
    Procedure IamAMessage(var M:TMessage);message wm_SomeMessage;
end;

用了Override 关键字后,编译器就会用新的方法替换VMT 中原先的方法。如果用Virtual 或Dynamic 替换Override 重新声明IAmAVirtual 和IAmADynamic,将是建立新的方法而不是对祖先的方法进行覆盖。同样,在派生类中如果企图对一个静态方法进行覆盖,在新对象中的方法完全替换在祖先类中的同名方法。

(6)方法的重载
就像普通的过程和函数,方法也支持重载,使得一个类中有许多同名的方法带着不同的参数表,能重载的方法必须用Overload 指示符标识出来,可以不对第1 个方法使用Overload。


下面的代码演示了一个类中有3 个重载的方法:

type
    TSomeClass=class
    Procedure Amethod(I:integer);overload;
    Procedure Amethod(S:string);overload;
    Procedure Amethod(D:double);overload;
end;

 

(7)重新引入方法名称
有时候,需要在派生类中增加一个方法,而这个方法的名称与祖先类中的某个方法名称相同。在这种情况下,没必要覆盖这个方法,只要在派生类中重新声明这个方法即可。但在编译时,编译器就会发出一个警告,提示派生类的方法将隐藏祖先类的同名方法。要解决这个问题,可以在派生类中使用Reintroduce 指示符,下面的代码演示了Reintroduce 指示符的正确用法:

type
    TSomeBase=class
        Procedure A;
    end;

    TSomeClass=class
        Procedure A;reintroduce;
    end;

 

(8)Self
在所有对象的方法中都有一个隐含变量称为Self,Self 是用来调用方法的指向类实例的指针。Self由编译器作为一个隐含参数传递给方法。

self,它到底指的是什么呢?我们还要从对象与类的关系谈起。 类是对将要创建的对象的性质的描述,是一种文档。这很重要:类只是一段描述性的文字,它并不会真去分配内存,无论在其中定义多少变量。 如果打个比方,类就是图纸,而对象就是根据图纸盖的房子。对象是真正在内存中存在的东西,是运行“实体”。根据一份图纸可以盖多个相似的房子,同样道理,根据一个类,可以创建多个类似的对象,这个过程叫做“实例化”。在delphi中使用对象技术,要遵循以下的步骤:

  • 定义一个类
  • 用该类声明一个名字(实质是一个指针)
  • 用该类实例化一个对象,并使它与先前的名字联系起来
  • 调用对象的方法或属性
  • 释放对象
下面我们写一个最简单的表达累加器功能的类
type
  TCount = class
    private
      FNum: integer; // 记录有多少个数字被累加
      FSum: integer; // 当前的累加和
    pubic
      procedure Add(n: integer) // 把整数n累加进去
      procedure clear; // 清零
      procedure show; // 显示当前信息
      constructor create; // 构造函数
  end;
 
....

使用的过程是这样的:

var a: TCount; // 这里只是声明了一个名字,并非真正地分配了一个对象
...... // a这个变量只占用4字节地内存
a := TCount.create; // 在堆空间中分配内存,并把首地址拷贝到a中。
a.add(5);
a.add(7);
a.show(); // 以上是调用a对象的有关方法
a.free; // free会去调用析构函数,完成堆空间的释放。

我们看这样一个问题:一个对象到底占用多大的内存空间?

答案是很小!因为在分配一个对象地时候,实际上只分配了类中定义的数据,而没有分配其中的函数所需要的空间。这些类中定义的函数(称为成员函数),与普通函数一样被存放在静态地址空间中。这样就引出了第二个问题:既然对象的数据和操作这些数据的方法不是存在一起,那么这些函数如何才能知道到底要操作哪个数据块呢(类可生成多个实例)?显然,最容易想到的解决办法就是让这些特殊函数带一个参数,是个指针类型,该指针指示要操作的数据块的首地址。
事实上,成员函数正是这样做的,它们有一个缺省的参数self,这是个指针类型,对于上边的例子,它的定义就是:TCount self。编译器在遇到调用a.add(5)的时候,把它解释为:TCount.add(a,5);把代表对象的数据块的地址送给add函数作为第一个参数--隐含的参数。说得本质一些就是:self是当前正在执行本函数的那个对象的数据块的首地址。

self既然代表对象自己,那么难道自己还用定义吗?看下边的代码:

procedure TForm1.button1click(sender: TObject);
var
  a: TButton;
begin
  a := Tbutton.create(self);
  ....
end;

 

2.属性
可以把属性看成是能对类中的数据进行修改和执行代码的特殊的辅助域。对于组件来说,属性就是列在Object Inspector 窗口的内容。下面的例子定义了一个有属性的简单对象:

TMyObject=class
    private
        SomeValue:integer;
        Procedure SetValue(a:integer);
    Public
        Property value:integer read SomeValue write SetValue;
    end;

procedure TMyObject.SetValue(a:integer);
begin
    if SomeValue<>a then
        SomeValue:=a;
end;

TMyObject 是包含下列内容的对象:

  一个域(被称为SomeValue 的整型数)、一个方法(被称为SetValue 的过程)和一个被称为Value 的属性。

SetValue 过程的功能是对SomeValue 域赋值,Value 属性实际上不包含任何数据。Value 是SomeValue 域的辅助域,当想得到Value 中的值时,它就从SomeValue读值;当试图对Value 属性设置值时,Value 就调用SetValue 对SomeValue 设置值。这样做的好处有两个方面:

  • 首先,通过一个简单变量就可以使外部代码访问对象的数据,而不需要知道对象的实现细节。
  • 其次,在派生类中可以覆盖诸如SetValue 的方法以实现多态性。

在Object Pascal 中的类实例实际上是指向堆中的类实例数据的32 位指针。当访问对象的域、方法和属性时,编译器会自动产生一些代码来处理这个指针。这时的对象就好像是一个静态变量。所以说,Object Pascal 无法像C++那样在应用程序的数据段中为类分配内存,而只能在堆中分配内存。

 

2.7 多态性

在Object Pascal 语言中定义的类的方法通常是“静态”的,也就是在编译和链接阶段就确定了对象方法的调用地址。面向对象的程序设计语言还可以在运行时才确定对象方法的调用地址,这种调用函数的方式叫做多态性,有时也称为动态联编或滞后联编。在Object Pascal 语言中,多态性是通过虚拟方法或动态方法实现的。
通常,可以将类中的方法定义为下面的3 种方式:

  • 静态方法。
  • 虚拟方法。
  • 动态方法。

在默认情况下定义的方法为静态方法,静态方法的调用地址在编译和链接的过程中就确定了。在类中,如果定义了一个方法,在它的派生类中也可以定义一个同样的方法。对于静态方法,通常叫做“静态重载”。


下面的例子说明了静态方法Infor 的调用情况:

program Project1;
{$APPTYPE CONSOLE}

type
    TPerson = class //人类
        procedure Infor; //显示信息
    end;

    TEmployee = class(TPerson) //职员类
        procedure Infor; //显示职员信息
    end;

procedure TPerson.Infor; //显示调用的是TPerson 类的Infor
begin
  Writeln ('TPerson.Infor');
end;

procedure TEmployee.Infor; //显示调用的是TEmployee 类的Infor
begin
    Writeln('TEmployee.Infor');
end;

var
    P1: TPerson; //声明一个人类的变量
    E1: TEmployee; //声明一个职员类的变量
    
begin
    P1:=TPerson.Create ;
    P1.Infor; //调用的是TPerson 类的Infor
    P1.Destroy;
P1:=TEmployee.Create; P1.Infor; //调用的是TPerson 类的Infor TEmployee(P1).Infor; //调用的是TEmployee 的Infor P1.Destroy;
E1:=TEmployee.Create; E1.Infor; //调用的是TEmployee 类的Infor E1.Destroy; end.

运行结果如下:

TPerson.Infor
TPerson.Infor
TEmployee.Infor
TEmployee.Infor

可以看到,在“静态重载”的情况下,Infor 的调用是根据对象的类型来确定的。

虚拟方法和动态方法也可以在派生类中被重载,通常称为“动态重载”。对象方法具体使用的并不是变量声明时指定的类的类型。将一个方法指明为虚拟方法或动态方法,需要使用指令字Virtual 或Dynamic。虚拟方法和动态方法在语意上是等价的,它们的主要不同之处在于运行时方法调用实现上。虚拟方法调用的速度比较快,而动态方法的代码数量比较少。一般在静态方法无法实现的情况下,可以考虑使用虚拟方法和动态方法实现动态联编。通常,虚拟方法在实现多态性方面的效率比较高。如果一个基类派生了很多子类,只是偶尔对方法进行重载,那么使用动态方法则更合适。

下面的例子说明了虚拟方法的定义与使用。

program Project1;
{$APPTYPE CONSOLE}

type
    TPerson = class //人类
        procedure Infor;virtual;
    end;

    TEmployee = class(TPerson) //职员类
        procedure Infor;override;
    end ;

    TCustomer = class(TPerson) //顾客类
        procedure Infor;override;
    end ;

procedure TPerson.Infor;
begin
    Writeln ( 'TPerson.Infor' ) ;
end;

procedure TEmployee.Infor;
begin
    Writeln( 'TEmployee.Infor');
end;

procedure TCustomer.Infor;
begin
    Writeln( 'TCustomer.Infor');
end;

var
    P1: TPerson; //声明一个人类的变量
    E1: TEmployee; //声明一个职员类的变量
   
begin
    P1:= TPerson.Create;
    P1.Infor; //调用的是TPerson 类的Infor
    P1.Destroy;

    P1:=TEmployee.Create;
    P1.Infor; //调用的是TEmployee 类的Infor
    TEmployee(P1).Infor; //调用的是TEmployee 类的Infor
    P1.Destroy;

    P1:=TCustomer.Create;
    P1.Infor; //调用的是TCustomer 类的Infor
    TPerson(P1).Infor; //调用的是TCustomer 类的Infor
    P1.Destroy;

    E1:=TEmployee.Create;
    E1.Infor; //调用的是TEmployee 类的Infor
    TCustomer(E1).Infor; //调用的是TEmployee 类的Infor
    E1.Destroy;
end.

运行结果如下:

TPerson.Infor
TEmployee.Infor
TEmployee.Infor
TCustomer. Infor
TCustomer.Infor
TEmployee.Infor
TEmployee.Infor

 

2.8 类运算符

在程序运行期间,可以使用Is 运算符和As 运算符来进行类信息检测和类型转换,通常也把这两个运算符称为运行时类型信息(RTTI:Runtime Type Information)运算符。


1.Is 运算符
Is 运算符用来检测一个对象在运行时的类的类型,具体形式如下:

object is class

如果返回值为True,那么对象Object 是类Class 或者是类Class 派生类的一个实例。如果对象为nil,返回值则为False。

 

2.As 运算符
As 运算符用来进行类型转换检测的,具体形式如下:

object as class

返回值为Object 的一个为Class 类型的引用。在运行期间,Object 必须是与Class 类兼容的一个类的对象或nil。通常为了避免类型不兼容,可以使用Is 运算符来进行类型判断。

 

下面的例子对Is 运算符和As 运算符进行了说明:

program Project1;
{$APPTYPE CONSOLE}

type
    TPerson = class //人类
    public
        Name:string; //姓名
    end;

    TEmployee = class(TPerson) //顾客类
    public
        DeptName:string; //地址名称
        procedure Infor; //只有子类具有"显示信息"的方法
    end;

    TCustomer = class(TPerson) //顾客类
    public
        DeptName:String;
        procedure Infor;
    end;

procedure TEmployee.Infor;
begin
    Writeln('姓名:',Name,'; 部门名称:',DeptName);
end;

procedure TCustomer.Infor;
begin
    Writeln('姓名:',Name,'; 部门名称:',DeptName);
end;

var
    P1: TPerson; //声明一个人类的变量
    E1: TEmployee; //声明一个顾客类的变量

begin
    P1:=TPerson.Create; //P1 为父类的对象
    P1.Name:='张三';
    if P1 is TCustomer then (P1 as TCustomer).DeptName:='人事部' ;
    if P1 is TCustomer then (P1 as TCustomer).Infor;
    P1.Free;

    P1:=TCustomer.Create; //P1 为子类的对象
    P1.Name:='冯六';
    if P1 is TCustomer then (P1 as TCustomer).DeptName:='人事部' ;
    if P1 is TCustomer then (P1 as TCustomer).Infor;
    P1.Free;

    E1:=TEmployee.Create; //E1 为子类的对象
    E1.Name:='高七';
    if E1 is TEmployee then (E1 as TEmployee).DeptName:='公关部';
    if E1 is TEmployee then (E1 as TEmployee).Infor;
    E1.Free;
    Readln;
end.

 

运行结果如下:

姓名:冯六; 部门名称:人事部
姓名:高七; 部门名称:公关部

 

这里有几点需要说明:

  • 当P1 调用TPerson 类的构造函数的时候,创建的是一个TPerson 的对象,不可以调用子类TCustomer 中特有的过程Infor,也不可以对子类中的特有属性DeptName 进行操作。在调用时要用Is运算符来判断P1 是否为TCustomer 类的对象。
  • TPerson 类的变量P1 被“创建”了两次,由于前后两次创建的类型不同,所以Is 运算符判断的结果不同。第2 次“P1 is TCustomer”返回的是True,然后进行类型转换并访问DeptName 属性和调用Infor 过程。

 

2.9 类方法和类引用

1.类方法
通常情况下,类中所定义的方法是被对象调用的。在调用构造函数Create 的时候,使用的是类,而不是具体的对象。类似地还可以定义一些类方法,它们对类进行操作,而不是对具体的对象进行操作。在定义类方法的时候,使用保留字Class 对过程或函数进行说明。
下面的例子说明了如何使用类方法:

program Project1;
{$APPTYPE CONSOLE}

type
    TEmployee = class //职员类
        Name: string; //职员姓名
        class procedure AddOne; //增加一个职员
        destructor Destroy;override; //析构函数,减少一个职员
    end;

var
    ENum :integer; //表示当前的职员数
    E1,E2: TEmployee; //声明职员类的变量

{ TEmployee }
class procedure TEmployee.AddOne;
begin
    ENum:=ENum+1;
end;

destructor TEmployee.Destroy;
begin
    ENum:=ENum-1;
    inherited Destroy;
end;

begin
    E1:= TEmployee.Create;
    E1.AddOne; //调用类方法改变变量ENum
    E1.Name:='李文武';
    Writeln( '职员数为:' ,ENum);

    E2:=TEmployee.Create;
    E2.AddOne;
    E2.Name:= '张化十';
    Writeln('职员数为:',ENum);
    E1.Free;
    E2.Free;
    Writeln('职员数为:',ENum);
    Readln;
end.

运行结果如下:

职员数为:1
职员数为:2
职员数为:0

在定义类方法的时候,标识符Self 将代表类方法中被调用的类。不可以使用Self 访问类的字段、属性和普通方法,但是可以通过Self 调用构造函数和其他类方法。

 

2.类引用
类引用(Class Reference)是一种数据类型,有时又称为元类(MetaClass),是类的类型的引用。
类引用的定义形式如下:

class of type

例如:

type SomeClass = class of TObject;
var AnyObj: SomeClass;

下面的例子说明了类引用的用法:

program Project1;
{$APPTYPE CONSOLE}

type
    TPerson = class //人员类
        Name: string; //姓名
    end;

    TEmployee = class(TPerson) //职员类
        DeptName: string; //部门名称
        procedure Infor; //显示职员信息
    end;

    CRef = class of TObject; //定义了一个"类引用"数据类型

var
    Employee: array[0..1] of TObject; //类的变量数组
    i: Integer; //循环变量
    CR:array[0..1] of CRef; //类引用数组

{TEmployee }
procedure TEmployee.Infor;
begin
    Writeln('姓名:',Name,';部门名称:',DeptName);
end;

begin
    CR[0]:=TPerson; //给类引用赋值
    CR[1]:=TEmployee;

    for i:=0 to 1 do
        begin
            Employee[i]:=CR[i].Create; //创建对象

            if Employee [i] is TEmployee then //判断对象的类型
            begin
                (Employee[I] as TEmployee).Name:='残月' ;
                (Employee[I] as TEmployee).DeptName:='人事部' ;
                (Employee[I] as TEmployee).Infor;
            end;
        end;
    Readln;
end.

运行结果如下:

姓名:残月;部门名称:人事部

注意:上面定义了一个类引用类型的数组,其中的两个元素的数值分别为不同的两个类的类型。

 
posted @ 2014-07-04 08:43  ivantang  阅读(957)  评论(0编辑  收藏  举报