第9章 Delphi面向对象程序设计.docx
《第9章 Delphi面向对象程序设计.docx》由会员分享,可在线阅读,更多相关《第9章 Delphi面向对象程序设计.docx(37页珍藏版)》请在冰豆网上搜索。
第9章Delphi面向对象程序设计
第9章面向对象程序设计
面向过程的程序设计着眼于系统实现的功能,采用自顶向下,逐步细化的方法进行功能分解直至建立系统的功能结构和相应的程序模块。
类(Class)是具有相同属性和操作的对象的集合。
类是进行数据抽象的基本单位。
每一个对象都是类的一个实例(Instance)。
所谓继承是父类(BaseClass)(基类)可以派生自己的子类(DerivedClass)(派生类),子类除了继承父类的属性和操作之外,还具有自己独特的属性和操作。
通信是实现各个不同对象之间消息传递的方法。
所谓消息实际上是一个类的对象要求另一个类的对象执行操作的指令。
9.1对象的基本概念
9.1.1对象的特性
一个对象,其最突出的特征有三个:
封装性、继承性、多态性。
1.对象的封装性
对象的封装特性是把数据和代码结合在同一个结构中。
将对象的数据域封闭在对象的内部,使得外部程序必须而且只能使用正确的方法才能对要读写的数据域进行访问。
2.对象的继承性
对象的继承性是指把一个新的对象定义成为已存在对象的后代。
新对象继承了旧类的一切东西。
3.对象的多态性
多态性是在对象体系中把设想和实现分开的手段。
多态的含义是指某一个标识符表示多种类型的变量,或者标识不同意义的函数或过程。
9.1.2从一个对象中继承数据和方法
在窗体上单击鼠标或用ObjectInspector的上端的ObjectSelector选中Form1对象,按键查阅他的在线帮助,会在Properties和Metehod中找到它的继承到的全部属性和当在工程中加入一个新窗体时,就等于加入了一个基本模型。
通过不断地在窗体中加入部件,就自行定义了一个新的窗体。
要自定义任何对象,都将从已经存在的对象中继承域和方法,建立一个该种对象的子类。
9.1.3对象的范围
一个对象的范围决定了它的数据域、属性值、方法的活动范围和访问范围。
在一个对象的声明部分声明的数据域、属性值、方法都只是在这个对象的范围中,而且只有这个对象和它的后代才能又拥有它们。
虽然这些方法的实际程序代码可能是在这个对象之外的程序库单元中,但这些方法仍然在这个对象的范围内,因为它们是在这个对象的声明部分中的声明的。
当在一个对象的事件处理过程中编写程序代码来访问这个对象的属性值、方法或域时,不需要在这些标识符之前加上这个对象变量的名称。
9.1.4对象共有域和私有域的声明
可以在对象的Public或Private部分加入新的数据域和方法。
Public和Private是ObjectPascal的保留字。
在Pbulic部分中声明其他库单元中对象的方法也可以访问的数据域或方法。
在Private部分的声明有访问的限制。
如果在Private中声明域和方法,那么它在声明这个对象的库单元外是不透明的,而且不能被访问。
Private中可以声明只能被本可单元方法访问的数据域和本库单元对象访问的方法。
9.1.5访问对象的域和方法
当想要改变一个窗体对象的一个域的某个属性,或是调用它的一个方法是,必须在这个属性名称或调用方法之前加上这个对象的名称。
同样想改变一个窗体对象中一个对象域的多个属性或调用多个方法时,使用with语句可以简化程序。
With语句在对象中可和在记录汇总一样使用。
9.1.6对象变量的赋值
如果两个变量类型相同或兼容,可以把其中一个对象变量赋给另一个对象变量。
只要赋值的对象变量是被赋值的对象变量的祖先
类型,就可以将一个对象变量赋给另一个对象变量。
9.1.7建立非可视化对象
1.声明一个非可视化对象
可以用如下的方法建立一个自己的TWorker非可视化对象
Type
TWorker=Class(TObject)
Title:
=String[20];
Name:
=String[20];
HourlyPayRate:
real;
FunctionCalculatePayAmount:
real;
end;
2.用Create方法建立对象实例
TWorker只是一个对象类型除非通过一个构造函数的调用从而被实例取代或创建,否则一个对象并不存储在内存中。
构造函数是一个方法,它为新对象配置内存并且指向这个新的对象。
这个新的对象也被称为这个对象类型的一个实例。
建立一个对象的实例,需要调用Create方法,然后构造函数把这个实例赋给一个变量。
如果想声明一个TWorker类型的实例,在访问这个对象的任何域之前,的程序代码必须调用Create。
Worker:
=Tworker.Create;
3.撤销对象
当使用完对象后,应该及时撤销它,以便把这个对象占用的内存释放出来。
可以通过调用一个注销方法来撤销的对象,它会释放分配给这个对象的内存。
Delphi的注销方法有两个:
Destroy和Free。
Delphi建议使用Free,因为它比Destroy更为安全,同时调用Free会生成效率更高的代码。
可以用下列的语句释放用完的Worker对象:
Worker.Free;
9.2类类型和对象
对象是类的实例(instance),即由类定义的数据类型的变量。
对象是实体,当程序运行时,对象为它们的内部表达占用一些内存。
对象与类的关系就像变量与类型的关系。
在ObjectPascal中,声明类数据类型使用保留字Class。
类类型声明的一般格式为:
Type
<类名>=Class(<父类名>)
<类成员>
End;
有关类类型的儿点声明:
(1)类名可以是任何合法的标识符,在Delphi中,类类型的标识符一般以T打头。
(2)Class是保留字,表示声明类型是类类型。
(3)Class后面的父类名表示当前声明的类是从父类名制定的类中派生出来的,声明的类称为父类的子类或直接后代,该子类将继承父类及所有祖先的所有成员。
(4)“父类名”是可以省略的。
(5)类类型声明中可以没有成员列表,如果需要,类类型可以有3类成员,分别是Field(字段)、Method(方法)、property(特性)。
(6)在类的声明中如果含有字段成员,那么字段成员的声明必需优先于特性和方法成员的声明。
(7)跟其他数据类型不同的是,类类型的声明只能出现在Program单元或UNIT单元最外层作用域的类型定义部分,而不能定义在变量声明部分或一个过程或函数内。
因此,类类型的作用域总是全局的。
(8)一旦声明了类类型,其使用同其他数据类型一样,可以创建这个类的多个实例(对象),所有创建的对象将共享该类的成员。
9.3封装
封装,是抽象数据类型(或基于对象)的特性。
似乎一谈到对象,能立刻想到的就是封装。
因为很容易就能把对象理解成所谓的黑匣子。
为什么要封装?
可以把程序按某种方法分成很多“块”,块与块之间可能会有联系。
每个块都有一个可变的部分和一个稳定的部分。
我们需要把可变的部分和稳定的部分分离开来,将稳定的部分暴露给其他块,而将可变的部分隐藏起来,以便于随时可以让它改变。
这项工作就是封装!
例如,在用类来实现某个逻辑时,类就是以上所说的“块”,实现功能的具体代码就是“可变的部分”,而public的方法(称为“接口”)则是“稳定的部分”。
9.3.1类级别的封装
类级别的封装是最常见的封装形式。
每个ObjectPascal的类,有四种访问级别:
private、protected、public、published。
其中,public的成员可以被外界的所有客户代码直接访问;published和public差不多,区别仅在于published的成员可以被Delphi开发环境的ObjectInspector所显示,因此一般将属性或事件声明于published段;private成员为类的私有性质,仅有类本身和友元可访问;protected成员基本与private类似,区别在于protected可以被该类的所有派生类访问。
在类级别的封装中,对外界的接口是public方法和published成员的集合,private和protected的集合则属于类的实现细节。
而对于该类的派生类来说,接口是public、published与protected的集合,而只有private部分为内部实现细节。
9.3.2单元级别的封装
单元级别的封装包含的含义有:
1.在一个Unit中声明的多个类,互为友元类。
2.在一个Unit的interface部分声明的变量为全局变量,其他Unit可见。
3.在一个Unit的implementation部分声明的变量为该unit的局部变量,只在该Unit可见。
4.每个Unit可有单独的初始化段(initialization)和反初始化段(finalization),可在编译器支持下自动进行Unit级别的初始化和反初始化。
ObjectPascal规定,声明在同一个Unit之中的多个类互为友元类,友元类之间可以互相访问所有数据,无论是public的,还是private的,或者是protected的。
也就是说,友元类之间没有秘密。
如下面的两个类:
type
TFriend1=class
private
FMember1:
Integer;
end;
TFriend2=class
private
Friend:
TFriend1;
public
functionGetFriendMember():
Integer;
end;
TFriend1和TFriend2之间可以互相访问私有数据成员:
functionTFriend2.GetFriendMember:
Integer;
begin
Result:
=Friend.FMember1;//访问了TFriend1的private数据
end;
虽然FMember1是TFriend1类的private数据,但在TFriend2中可以访问,这是合法的。
粗看起来,友元类似乎破坏了封装。
但其实适当地使用友元的特性,可以增强封装性。
有时一个类的两部分可能会具有不同的生命周期,也许用户会将这两部分拆分成两个相关的类,此时两个类之间可能会互相访问彼此的数据,而数据成员一般都被置于private节中。
如果避免使用友元,则只能要么将数据置于public节中,要么提供GetXXX、SetXXX之类的方法。
将数据置于public的做法是非常罕见的,而提供GetXXX、SetXXX之类的方法也绝非优良设计,这些做法其实都破坏了封装性。
如何保持封装性呢?
答案就是使用友元!
ObjectPascal的单元文件被分成了两个部分:
interface和implementation。
如同类的封装一样,Unit的这两部分分别为接口和实现细节。
因此,interface部分对外是可见的,声明在interface段中的所有函数、过程、变量的集合,即单元文件作为一个模块的对外接口,而implementation部分对外是隐藏的。
而为单元文件提供初始化和反初始化机制,则保证了单元的独立性,其作用如同类的构造函数与析构函数,单元的运作由此便可脱离对其他模块的依赖。
以下是一个完整的Unit示例:
unitUnitDemo;
interface
usesWindows;
procedureProc1();//某功能函数
procedureInitUnit();//单元初始化函数
procedureUnInitUnit();//单元反初始化函数
var
g_nGlobalVar:
Integer;//全局变量
implementation
var
l_nLocalVar:
Integer;//单元级别的局部变量
procedureInitUnit();
begin
l_nLocalVar:
=0;
……//其他初始化工作
end;
procedureUnInitUnit();
begin
……//反初始化
end;
procedureProc1();
begin
……//一些代码
end;
initialization//初始化段
InitUnit();//调用InitUnit()以初始化单元
finalization//反初始化段
UnInitUnit();
end.
无论是单元的封装,还是类的封装,封装的目的都是一样的,即简化用户接口,隐藏实现细节。
正如小节语义的“类”和“对象”中所述,封装的难点在于如何设计接口。
首先,必须保证接口是功能的全集,即接口能够覆盖所有需求。
不能完成必要功能的封装是毫无意义的。
其次,尽量让接口是最小冗余的。
这是为了简化客户的学习,难用的封装是容易被人遗忘的。
冗余接口的存在是被允许的,但必须保证冗余接口是有效的。
也就是说,增加这个冗余接口会带来非常大的好处,比如性能的飞速提升。
最后,要保证接口是稳定的。
将接口和实现分离,并将实现隐藏,就是为了能保护客户的代码在功能实现细节改变的情况下,不必随之改变。
三天两头改变接口的封装是惹人讨厌的。
记住一个原则:
一旦接口被公布,永远也不要改变它!
9.4继承的本质
继承是为了表现类和类之间的“是一种”关系。
有了继承之后,构建多层次的类框架成为可能。
同时,它也是面向对象中的另一个核心概念——多态的存在基础。
因此,继承是面向对象语言必不可少的特性,只支持封装而不支持继承的语言只能称为“基于对象”(Object-Based)而非“面向对象”(Object-Oriented)。
在利用语言提供的继承特性之前,有必要先了解一下语言本身关于继承的一些特性及实现。
9.4.1语言的“继承”
首先要了解从语言层次的视角对“继承”概念的理解与语言对其的实现支持。
继承关系也被称为派生。
继承的关系中,被继承的称为基类;从基类继承而得的,称为派生类。
比如说,类B从类A继承而得,则B为派生类,A为基类。
在ObjectPascal语言中,定义继承关系的语法:
TB=class(TA)
表示TB从TA继承(派生),TB是派生类,而TA为基类。
ObjectPascal只支持C++中所谓的public继承,即派生类中基类的public成员在其中仍然是public的,基类的protected成员在派生类中仍然是protected的,派生类无法访问基类的private成员。
Public继承在语义上严格地奉行“是一种”关系。
也就是说,类B若派生自类A的话,那么在任何时候,都可以称“B是一种A”。
因此,在设计继承层次时,也应该注意,如果B不是在任何时候都可以被当作A,那么就不可以将B从A派生。
ObjectPascal只支持单继承,即每个派生类只能有一个基类,由此可以保证每个派生类中,只有惟一一份基类子对象。
也许有些读者还不清楚什么是基类子对象,或者不清楚上面这句话的具体含义是什么。
下面就来介绍一下。
在本节类和对象的本质中曾经提到过,对象所占的内存空间大小取决于这个对象中的数据成员。
也就是说,每一个对象实例中,都包含了它所有的数据成员。
更进一步,在允许继承的情况下,每个派生类的对象实例所占内存空间的大小,不但取决于自身的数据成员,还要加上其基类的数据成员。
每一个类的实例对象所占的内存空间,是其自身的数据成员与其所有基类(因为基类可能还有基类)的数据成员(不论是private的,还是public的)所占内存空间的总和(不考虑“按位对齐优化”的情况)。
每一个派生类的实例对象,内部都包含了一个完整的基类实例对象,这个完整的基类实例对象,就称为“基类子对象”,因为基类的对象永远小于或者等于派生类的对象。
虽然派生类对象无法访问基类子对象中的private的数据,但是,这些数据是的确存在并且占用内存空间的。
下面以一个示例程序来说明派生类对象和基类子对象的关系以及它们的内存布局情况。
先定义一个三层的继承层次:
type
TBase=class
public
FBaseMember1:
Integer;
FBaseMember2:
Integer;
end;
TDerived=class(TBase)
public
FDerivedMember:
Integer;
end;
TDerived2=class(TDerived)
public
FDerived2Member1:
Integer;
FDerived2Member2:
Integer;
end;
TBase是基类,TDerived派生自TBase,因此它是TBase的派生类,但由于TDerived2派生自TDerived,因此,TDerived同时也是TDerived2的基类,而TDerived2是TDerived的派生类。
在此定义了一个三层的继承层次,但由于成员方法是不占用对象实例的内存空间的,因此,为方便说明起见,不定义方法成员。
定义完相关类后,在Application的主Form(Form1)上放上一个ListBox(name为:
lst_rs)和一个Button。
然后在Button的OnClick事件中加入代码,使得对象位置信息显示在ListBox中。
该程序含有两个单元,名称为clsinherite.pas的单元中仅定义了TBase、TDerived、TDerived23个类(如前定义),另一个为主Form的代码单元,其代码清单如下:
unitUnit1;
interface
uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,
Forms,
Dialogs,StdCtrls;
type
TForm1=class(TForm)
Button1:
TButton;
lst_rs:
TListBox;
Label1:
TLabel;
procedureButton1Click(Sender:
TObject);
private
{Privatedeclarations}
public
{Publicdeclarations}
end;
var
Form1:
TForm1;
implementation
usesclsinherit;
{$R*.dfm}
procedureTForm1.Button1Click(Sender:
TObject);
var
Obj:
TDerived2;
begin
Obj:
=TDerived2.Create();
withlst_rs.Itemsdo
begin
Add('对象大小:
'+IntToStr(Obj.InstanceSize));
Add('对象首地址:
'+IntToStr(Integer(Obj)));
Add('TBase成员首地址:
'+IntToStr(Integer(@Obj.FBaseMember1)));
Add('TDerived扩展成员(FDerivedMember)首地址:
'+
IntToStr(Integer(@Obj.FDerivedMember)));
Add('TDerived2扩展成员(FDerived2Member1)首地址:
'+
IntToStr(Integer(@Obj.FDerived2Member1)));
end;
Obj.Free();
end;
end.
Button1Click()方法创建一个TDerived2类的一个实例对象,然后将对象首地址、对象大小及其所有数据成员(包括从基类中派生而得来的数据成员)的地址在ListBox中显示出来。
运行程序并单击“开始”按钮,程序结果如图所示。
派生类内存布局演示程序界面
结果在图中也显示了:
对象大小为24字节;首地址为13443344(也许在各位的计算机上运行该地址值有所不同,没有关系,在此只关心这些地址值之间的差值);TBase成员首地址为13443348;TDerived扩展成员首地址为13443356;TDerived2扩展成员首地址为13443360。
推算可知,对象所占内存地址范围为13443344~13443367。
TBase成员首地址和整个对象首地址之间存在4个字节的差值,这个空缺还是那个指向VMT的指针。
TBase的两个整型数据成员占用8个字节,因此TDerived的
FDerivedMember的首地址就是13443356了。
同理,再步进4个字节,就是TDerived2的FDerived2Member1的首地址了。
根据上面的演算,可以画出Obj对象的内存布局图,如图所示。
派生对象内存布局
(图中3个深色矩形框,由里向外分别表示TBase、TDerived、TDerived2的完整实例)从图中可以清楚地看到,Obj对象(最外层的深色矩形框)中完整地包含了TDerived类的实例对象(中间层的深色矩形框)和TBase类的实例对象(最内层的深色矩形框)。
最内层的矩形框表示了TDerived2类实例对象中的TBase基类子对象,中间层的矩形框表示了TDerived2类实例对象中的TDerived基类子对象。
注意:
每个基类子对象都是完整的。
需要特别说明一下的是,此处为了便于Button的OnClick事件中的代码可以直接访问类的所有数据成员并将它们的地址打印出来,因此在定义类时,将所有数据成员都声明为public。
但实际程序运行结果(指每个数据成员所在地址)并不依赖于它是处于public的还是private的。
也就如同上面所说的,即使派生类对象无法访问基类子对象中的private的数据,它们依然是存在并占用内存空间的,无法访问它只是因为编译器为它做了额外的保护。
9.4.2语义的“继承”
了解语言对于继承的理解与实现支持,对于设计是有所助益的。
但是,设计更多的时侯是根据语义的。
语义上的“继承”,更多的是作为一种“特化”的机制。
也就是说,能够被继承的类(基类)总是含有并且只含有所抽象的那一类事物的共性,当需要抽象该类事中的某一种特例时,将表示特例的类从基类继承(派生),派生类含有这类事物的所有共性,并且自己拥有特性。
例如,“水果”这个概念抽象了很多事物,这些事物有一些共性,如可以食用、属于农作物等。
“苹果”这个概念表示了一种特殊的水果,它作为“水果”的特例,包含水果的一切属性——可以被食用、属于农作物等,同时它也包含自己的特性——果实为圆形、味道甜美等。
语义上的“继承”表示“是一种”的关系,派生类可以被看作“是一种”基类,这