Delphi对象模型 (PART IV)
Delphi对于面向对象编程的支持丰富而且强大。除了传统的类和对象,Delphi还提供了接口,异常处理,多线程编程等特性。这一章节深入讲解了Delphi的对象模型。读者应当对标准的Pascal比较熟悉,并且对有关面向对象编程的基本法则有一定了解。
(本文的英文原文将Delphi与Object Pascal统一表述为Delphi,可能有概念不清之嫌疑。但在大多数情况下,相信读者能够根据上下文来判定文中所述之Delphi的具体含义——译者注。)
Object Life Cycle对象的生命周期
对于绝大多数对象,你调用构造器创建它,使用它,然后调用Free将它释放。Delphi替你处理了所有其他细节。或许,有时,你需要了解更多有关Delphi对象模型内部的实现机制。例2-8显示的是用于模拟Delphi创建和释放一个对象的方法。
例 2-8:对象的生命周期
type
TSomething = class
procedure DoSomething;
end;
var
Ref: TSomething;
begin
Ref := TSomething.Create;
Ref.DoSomething;
Ref.Free;
end;
//在构造器中被隐藏代码类似于这样:
function TSomething.Create(IsClassRef: Boolean): TSomething;
begin
if IsClassRef then
try
//获得新的对象实例
Self := TSomething.NewInstance;
//NewInstance初始化对象,功能与InitInstance类似。假如你覆盖了NewInstance,那么,
//不要调用inherited NewInstance,而应当调用InitInstance。
//这个调用正如下面所示,因此你可以知道究竟发生了什么,
//但是请记住通常Delphi实际上并不调用InitInstance。
InitInstance(Self);
//做构造器真正要做的工作,无需用到类引用。
//注意,Delphi并不真的对构造器进行递归调用。
Self.Create(False);
Self.AfterConstruction;
except
//若有任何异常发生,Delphi自动调用对象的析构器。
Self.Destroy;
end
else
Self.Create(False);
Result := Self;
end;
//构造器中被隐藏的代码类似于这样:
procedure TSomething.Destroy(Deallocate: Boolean);
begin
if Deallocate then
Self.BeforeDestruction;
// Delphi并不真的递归调用析构器,但这里是析构器真正发生作用的地方。
Self.Destroy(False);
if Deallocate then
begin
// Delphi并不真的调用CleanupInstance而是调用FreeInstance来做清理的工作。
// 假如覆盖了FreeInstance,不要调用inherited FreeInstance,而应当调用CleanupInstance
// 来清理字串、动态数组以及可变类型字段。
Self.CleanupInstance;
//调用FreeInstance来释放对象占用的内存。
Self.FreeInstance;
end;
end;
Access Levels访问级别
类似于C++和Java,Delphi提供了不同的访问级别控制机制,来决定一个对象是否能其他对象的字段,方法,以及属性等。访问级别分为以下几种:
private 私有的
私有的方法,只能被该类自己的方法以及在同一单元的实现部分中定义的方法、过程和函数访问。Delphi没有C++风格的友类声明也没有Java风格的包级别上的访问控制(package level access)。在Delphi中与之等同的是将包或者友类在同一单元声明,这样该单元中的所有类的私有及受保护部分都可以被访问。
protected 受保护的
受保护的方法,可以被该类以及派生类的任何方法访问。派生的类可以处于不同的单元中。
public 公用的
公开的方法没有访问限制。任何方法,函数,或者过程都可以访问公用声明的部分。除非使用$M+编译指示符,否则缺省的访问级别是公用的。
published发布的
发布的声明与公用的声明基本上相同,唯一的不同是Delphi为发布的声明保存运行时间信息。有些声明不能是发布的;详见第三章内容。如果类或者基类使用$M+指示符,则缺省的访问级别为发布的。
提示:
Delphi的IDE在Form最开始没有命名的部分声明字段和方法。因为TForm继承自TPersistent,而在TPersistent中使用了$M+指示符,因此最开始部分的访问级别为发布的published。换句话说,IDE将字段和方法声明为发布的。当Delphi装载一个Form描述文件(.dfm文件)时,它依赖于发布的信息来创建该Form对象。IDE依赖于Form类的最开始未命名部分的声明。假如你修改那一部分的东西,可能会有导致IDE的Form编辑器不可用的危险。
automated 自动的
自动的声明与公开的声明很接近,唯一的不同就是Delphi将保存更多的运行时间信息以支持OLE自动化服务。自动化的声明已经成陈旧了;你可以转而使用Delphi的类型库编辑器,但现在,他们为了向后兼容而保留了一部分。Delphi的下一版本可能会将这些部分彻底根除。第三章更进一步的阐述有关自动类型的声明。
派生的类可以提高属性的访问级别。通过在新的访问级别上重新声明属性来做到这一点(比如将首受保护的改为公开的)。但是不能降低一个属性的访问级别,并且你不能改变一个字段或者方法是否可见。你可以通过覆盖一个虚方法,或者声明一个覆盖方法在同样或者更高一级的访问级别上,但你不能降低访问级别。
Hiding a Constructor隐藏构造器
有时,一个类不用做公用的用途,而只是作为其他类的一个辅助类。这种情况下,你可能希望该辅助类的Create是私有的或者是受保护的,但这需要技巧。TObject声明了一个公开的构造器:Create.因此即使该辅助类的构造器是私有的或者受保护的,你依然可以调用自TObject继承而来的Create构造器。
尽管你不能改变继承来的Create构造器的访问权限,你还是可以通过另外一个公开的构造器来隐藏它。因为假如派生的构造器被调用,将引发一个异常。如下例:
type
TPublic = class;
TPrivateHelper = class
private
//TPublic是唯一一个被允许调用真正的构造器的类
constructor Create(Owner: TPublic);
overload;
public
//隐藏TObject.Create防止有人意外调用以试图创建TPrivateHelper的实例
constructor Create;
reintroduce; overload;
end;
TPublic = class
private
fHelper: TPrivateHelper;
public
constructor Create;
destructor Destroy;
end;
constructor TPrivateHelper.Create;
begin
raise Exception.Create('Programming error')
end;
constructor TPublic.Create;
begin
//这是唯一一个TPrivateHelper能被创建的地方
fHelper := TPrivateHelper.Create(Self);
end;
Properties属性
属性看起来更象是字段,但可以起到与方法一样的作用。属性替代了读取者和设置者(有时也称为getter和setter),但更加机动和强大。属性对于Delphi的IDE而言非常关键,同时,我们也可以在其他许多场合使用属性。
属性由一个读者和一个写者来负责读取和设置属性的值。读者(Reader)可以是一个字段名,一个集合字段的选择器,或是返回该属性值的一个方法。写者(writer)可以是一个字段名,一个集合字段的选择器或者可以设置该属性值的一个方法。你可以省略写者,那么该属性为只读属性。当然,也可以省略掉读者以创建一个只写的属性,但是使用这么一个怪怪的属性将很受限制。同时省略读者和写者是没有意义的,因此Delphi不允许你这么做。
大多数的读者和写者是字段名称或方法名称,你也可以将其引向部分集合字段(记录和数组)。如果一个读者或写者指向一个数组元素,那么数组的索引必须是常量,并且该字段不能为动态数组。纪录和数组可以嵌套,甚至你可以使用可变类型的记录。例2-9展示了一个扩展的矩形类型,与Windows的TRect类型相似,但它是一个类,有属性和方法。
例2-9:属性的读者与写者
TRectEx = class(TPersistent)
private
R: TRect;
function GetHeight: Integer;
function GetWidth: Integer;
procedure SetHeight(const Value: Integer);
procedure SetWidth(const Value: Integer);
public
constructor Create(const R: TRect); overload;
constructor Create(Left, Top, Right, Bottom: Integer); overload;
constructor Create(const TopLeft, BottomRight: TPoint); overload;
procedure Assign(Source: TPersistent); override;
procedure Inflate(X, Y: Integer);
procedure Intersect(const R: TRectEx);
function IsEmpty: Boolean;
function IsEqual(const R: TRectEx): Boolean;
procedure Offset(X, Y: Integer);
procedure Union(const R: TRectEx);
property TopLeft: TPoint read R.TopLeft write R.TopLeft;
property BottomRight: TPoint read R.BottomRight write R.BottomRight;
property Rect: TRect read R write R;
property Height: Integer read GetHeight write SetHeight;
property Width: Integer read GetWidth write SetWidth;
published
property Left: Integer read R.Left write R.Left default 0;
property Right: Integer read R.Right write R.Right default 0;
property Top: Integer read R.Top write R.Top default 0;
property Bottom: Integer read R.Bottom write R.Bottom default 0;
end;
Array properties数组型属性
数组型属性总是与数量有关,并且带有数组的特性。数组型属性不能被发布,但有许多其他用途。数组的索引可以是任何类型的,并且你可以使用多维的数组。对于数组型属性而言,你必须使用读者和写者的方法,因为你没有办法将一个数组型的属性直接映射到一个数组型的字段上。
可以将其中的一个数组型属性指定为默认的。则你可以直接使用对象引用以及一个数组标号来访问该项属性而无需指明属性名称,如例子 2-10所示。
例 2-10:使用缺省的数组属性
type
TExample = class
...
property Items[I: Integer]: Integer read GetItem write SetItem;
property Chars[C: Char]: Char read GetChar write SetChar; default;
end;
var
Example: TExample;
I: Integer;
C: Char;
begin
Example := TExample.Create;
I := Example.Items[4]; //必须显式的指明属性名称
C := Example['X']; //该数组型属性时缺省的
C := Example.Chars['X']; //效果如前
PartIV