在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
类和类成员概述 因为本书是讲"Delphi精要",所以对于面向对象理论中的类的概念,就不再使用什么"禽兽 | 家禽 | 鸡鸭鹅"之类例子来讲解了,如果大家对类的基本概念还不是很理解,那么可以参阅相关资料和书籍... 因为本书是讲"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 property WindowState: TWindowState read FWindowState write SetWindowState; {声明一个特殊的属性——事件OnDestroy,和WindowState不同,OnDestroy的存取都是 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,
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,
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.
因为声明虚方法的目的都是供子类覆盖,所以虚方法一般应该声明在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方 procedure AnotherProc(S: String); overload; procedure AnotherProc; overload; end; 为什么编译器不允许在published出现多个同名的方法呢?别忘了在前面我们说过:published区的类成员会生成运行时类型信息的,而类成员是通过名字区分的。因此,这时候编译器无法为成员AnotherProc生成运行时类型信息。 重载的概念对于普通的过程和函数也是适用的,实际上方法重载是从普通过程和函数的重载引申而来的。我之所以没有将重载内容放在3.2节,是因为方法重载更加复杂,在这里可以更加全面地来阐述它。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论