第一章在Delphi中使用接口
1.1 定义接口:
目的:什么是接口,以及和抽象类的关联以及不同点。
抽象类(备注理解接口最简单的方法)
永远不能创建一个抽象类的实例;
本身不能实现功能,依靠派生类实现;
接口
被申明为interface类型。接口名从字母I开始。类类型名从T开始。
所有的接口从IUnknown继承;
不能创建接口实例;
不能在接口中指定范围指示。所有的方法都是公有型(public),不能在接口中申明包括范围指示;
不能申明变量;接口只能决定提供什么样的功能,对于如何完成功能没有限制;
接口中申明的所有函数和过程,概念上都是虚抽象函数和过程;因此申明时不能带Virtual;
接口是不变的;
1.2申明一个接口
目的:如何声明一个接口
常用的接口:CLSID、IID、和LIBID;
GUID(Globally Unique Identifier)全球唯一标示符: CoCreateGuid产生(API);
CLSID(Class Identifier):代表COM对象的类别,代表COM对象的CoClass;
IID(Interface Identifier): 代表一个COM对象的接口;
例如:如何建立COM对象的方法;
var
iRoot: IUnkown;
begin
...
iRoot := CreateComObject(ProgIDToClassID('Project1.Interface1'));
//取得实体样例
...
end;
应用程序在内存中建立了COM对象的实体样例;
注意: CreateComObject回传的是COM对象的实体样例的IUnknown接口;
APPID(Application Indentifier): 应用程序ID;
CATID(Category Identifier): COM组件实现的组件类型;
LIBID(Library Identifier): COM对象实现的Type Library代表的ID;
ProgID: 有意义的字符串代表特定的CLSID;
1.3 实现接口
目的:如何实现接口
实现IUnknown: QueryInterface、 _AddRef、 _Release
使用TInterfaceObject来自动实现Iunknown,否则的话自己要实现上面的方法。
创建、使用及销毁接口: create;指向接口的指针不访问任何信息;自动释放、强迫销毁一个接口将变量置为nil
注:delphi自动创建和销毁接口。
获取单个接口的指针:
直接分配:类与他们实现的接口类型兼容的
GetInterface(const IID: TGUID; out obj):判断对象是否支持一个接口
as操作符:对象支持特定的接口(对象不支持接口就错的话,可以拦截错误);
as自动调用计数功能;
1.4 高级多级接口问题
目的:在一个类中实现多个接口
在一个类中实现多个接口
TXY = class(TInterfacedObject, IXX,
IYY): 类TXY 实现了IXX和IYY接口的所有方法。
多个接口不是多重继承:TXY有且只有一个基类TInterfacedObject;
方法分辨字句:当接口方法在类中实现时,方法分辨子句可使用改变他的名称
TXY = class(TInterfacedObject, IXX, IYY)
procedure IXX.pxy = pxy1
procedure IYY.pxy = pxy2
接口授权:一个接口的实现授权给另一个类:一个类包含针对另一个类的指针。
内部类: 实现一个或多个接口的功能性;
外部类:简单的将这些方法传递给内部类,而不是重新实现接口;
接口属性:可以定义只读、只写、或者读写属性;
但是所有访问都必须通过访问函数,因为接口不能定义存储。
1.5 小结
目的:如何在delphi应用程序中内部使用接口,了解delphi语言要素的接口。
申明一个接口;
在类中实现接口;
实现IUnknown所需要的功能;
自动对象析构的处理;
在类中实现多个接口;
将一个接口的实现授权给一个内部对象;
定义并实现接口属性
[第一章 接口][第二章 接口与COM][第三章 类型库][第四章自动化][第五章 ActiveX][末尾]
第二章 接口与COM
2.1 GUIDs 和 COM
目的:
CLSID: Class ID是GUID一个具体的类型的名称,注册表 HKEY_CLASSES_ROOT/CLSID
每个接口CLSID或GUID都代表一个COM接口的实现
COM对象: TCOMObject继承(TInterfacedObject不提供实现COM对象的必要功能)
Hresult: 特殊类型的返回值,意味着函数调用成功还是失败。
OleCheck: 检查函数调用可能产生的错误;当调用返回HResult的COM函数时应使用该函数;
类厂(Class Factory):
COM对象不是由程序直接例示的;
COM使用类厂来创建对象;
类厂是一个对象,目的就是创建其他对象;
每一个COM都有一个相关的类厂,负责创建在服务器中实现的COM对象;
类厂把COM从实际构造一个对象的过程中分离出来,COM没有对象构造过程
类厂支持IClassFactory接口:
IClassFactory只定义2个函数CreateInstance和LockServer
CreateInstance函数: 负责创建类厂涉及的COM对象的实例的函数;
LockServer: 保持服务器在内存中,一般不要调用他;
类厂中的双重类:
2.2 进程内的COM服务器(In-Process COM Server)
目的:理解进程内的COM服务器
共性:有一个InprocServer32的子键、所有进程内服务器都输出以下四个标准函数;
DllRegisterServer: 2种方式自动调用
IDE的Register ActiveX Server菜单
Windows的命令行应用程序RegSvr32.exe(或Boland应用程序TRegSvr)
DllUnregisterServer: 是DllRegisterServer的逆进程,移走放在注册表中的条目;
DllGetClassObject:
负责提供给COM一个类厂,该类厂用语创建一个COM对象;
每个COM服务器将实现它输出的每个COM对象的类厂;
DllCanUnloadNow: 返回S_True,CO在内存中移走COM服务器;如果返回S_False
线程支持(Threading
Support): 只适用于进程内服务器;被保存在注册表中;线程模型如下
Single-Thread
Apartment(STA):实际上根本没有线程支持,所有对COM服务器的访问由Windows顺序执行,不必考虑同步存取的问题。
Mutli-Threaded Apartment(MTA):
允许同时有多个线程存取COM对象,必须控制不同线程同步存取的程序代码;
Both Apartment: 可以执行在MTS或STA中;
自由的:
注册服务器(registering the Server):所有的COM服务器都需要Windows注册表来工作。
定制构造函数(Custom constructors): delphi中COM对象的基类包括一系列的非虚构造函数。
只需重载Initialize方法;
不要试图重载一个COM对象的构造函数;
创建一个进程内COM对象
function CreateComObject(const ClassID: TGUID): IUnknown;
CoCreateInstance内部创建负责创建COM对象类厂的实例,然后使用类厂来创建对象,创建完后COM对象后,类厂被销毁;
虚方法表(Virtual Method Tables):
接口实现为独立的方法表,该表实现在内存中紧靠VMT的地方;
不同的接口占据不同的内存部分,不能简单的把一个接口值赋给另外一个接口;
通常使用as操作符从一个接口转换为另外一个接口;
2.3 进程外COM服务器(Out-Of-Process COM Server)
目的:理解进程外COM服务器
进程外服务器是在exe中实现;
实例化(Instancing): 创建多少个客户需要的实例;可以支持三个实例化方法中的一个;
Single Instace(单实例):
只容许一个COM对象的实例
每个需要COM对象实例的应用程序将产生COM服务器的单独拷贝;
Multiple Instance(多实例):
指COM Server可以创建一个COM对象的多个拷贝;
客户请求COM对象的一个实例时,由当前运行的服务器创建COM对象的一个实例;
Internal Only(内部实例):用于不被客户应用程序使用的COM对象;
调度数据(Marshaling
Data):
一个可执行的程序不能直接访问另一个可执行程序的地址空间;
Windows通过调度(Marshaling)进程在调用应用程序和进程外COM服务器之间移动数据;
自动化兼容
SmallInt、Integer、Single、Double、currency、TDateTime、WideString、Idispatch、Scode、WordBool、Olevariant、IUnknown、ShortInt、Byte;
记录和数组不能自动调度;
2.4 Variant 数组
目的:如何使用Variant 数组;
Variant:
一种可以拥有各种数据类型;
也可以告诉目前存储的数据是什么类型(通过使用VarType函数);
可以给相同的Variant分配不同的数据类型,只要Variant包含数字值就可以执行算法;
variant数组只不过是variant型的数组,不必包含同类型的数据;
variant数组的创建方法:
function VarArrayCreate(const Bounds: array of Integer; VarType:
integer): variant;
Bounds: 告诉数组的上下界;
VarType: 决定了数组的中存储什么类型的数据。
例如:创建数组的数组,
可以模仿任何类型的数据结构类型:
VarArrayX := VarArrayCreate([1,10], varVariant);
数组的单个元素可以装载一个数组: VarArrayX[1] := VarArrayCreate([1,5],
varVariant);
function VarArrayOf(const Values: array of Variant): Variant;
运行时用于创建一维数组;
可以创建全异的数值数组;
例如: MyArray := VarArrayOf(['李维', 30, '60', 60.369,
'China']);
使用Variant数组:与使用标准Delphi数组类似;
VarArrayLowBound、VarArrayHighBound(与数组的low、high类似)计算边界;
VarArrayDimCount:计算数组的维数;
2.5 小结
目的:接口GUID及进程COM服务器的基本知识了解;
Guid是什么;以及为什么Guid对COM是如此重要;如何创建Guid以供自己使用;
进程内COM服务器,创建及如何使用;
Variant数组; 是什么、及如何使用;
进程外COM服务器的介绍、调度及Windows可自动调度的类型;
只有能理解虚方法表(VMT)的面向对象的语言才能访问只有虚方法表的COM服务器
[第一章 接口][第二章 接口与COM][第三章 类型库][第四章自动化][第五章 ActiveX][末尾]
第三章 类型库
3.1 定义类型库
目的: 类型库是什么,和它的作用是什么。
使用类型库的好处:
编写自动化控制时早期连接(Early Binding);
许多编译器可以从一种类型库中自动生成针对特定编程语言的代码;
实用程序可以用来读取并显示有关包含类型库的COM服务器的信息;
在COM客户和服务器之间自动参数调度;
类型库对某些COM服务器是必须的,例如:自动化服务器和ActiveX控件;
TTypedComObject: Delphi由该类及其派生类提供了对类型库的支持;
Delphi自动创建XXXX_TLB.pas文件;
COM由TTypedComObject派生,而不是TComObjectFactory.Create;
初始时,delphi调用了TTypedComObjectFactory.create、而不是TComObjectFactory.Create;
3.2 使用delphi来创建类型库
目的: 如何使用Delphi来创建类型库的基本知识;
可以使用IDL(Interface
Definition LAnguage 接口定义语言)编码类型库;
类型库编辑器:
工具条:可以添加接口、方法、以及属性到COM服务器中;
注:工具条上可以通过点击鼠标右键弹出的菜单中选择Text Labels命令打开工具条的标题;
Interface(接口): 自动为每一个新建的接口产生一个GUId;
Dispinterface(派遣接口): 与接口类似,但是使用不同的派遣机制调用服务器中的方法;
CoClass(): 类别的定义,被指定给实现接口的COM对象;
使用COM对象之前必须先使用一种方法从类别定义建立真正的类别对象(变量);
再从类别对象取去需要的接口;
最后再从取得的接口中调用需要执行的方法或需要存取的属性;
Enumeration(枚举): 与枚举类型类似;有整数ID来指明,而不是通过集合类型;
Constant(常量):只有在枚举下才有用;可以编辑Name、Value属性;
Alias(别名): 用于定义用户要包括一条记录类型(record)或联合类型(union)中的类型;
Record(记录类型): 记录结构;
Union(联合类型): 等同于Pascal Variant类型;
Module(模块): 模块是方法和常量的集合;
Method(方法): 输入参数(in)、输出参数(out)、可变参数(var);
Property(属性): 只读、只写、读写三种;
Refresh(更新): 使Delphi更新源文件;
Register(注册): 编译COM服务器并把服务器注册到Windows中;
Export(输出到IDL): 在MIDL或Corba格式中很有用;
friend
对象列表(Object List):显示服务器中定义的接口、方法等;
页控件(Page control):显示对象列表中当前选中的接点的类型信息;
类型库也是一种资源;
不必在客户应用程序中直接读取类型库;
3.3 小结
目的: 类型库的运用;
类型库是什么;
如何创建一个类型库;
如何编译一个类型库类给自己的服务器添加方法、属性及枚举类型;
如何读取类型库的信息;
[第一章 接口][第二章 接口与COM][第三章 类型库][第四章自动化][第五章 ActiveX][末尾]
第四章 自动化
通过自动化操作类型库;接口和自动化;Variants和自动化;派遣接口;双重接口;自动化ADO;
4.1 定义自动化
目的: 自动化以及客户通过使用接口、Variants派遣接口和双重接口创建并使用自动化服务器;
自动化是一种从应用程序的内部自动控制另一个应用程序的方法。
获取自动化服务器的COM对象的两种主要访问方法:
接口:早期连接(early binding)
早期连接是指对接口方法的所有调用在编译时检查参数是否正确;
Variants: 后期连接 (late binding)
后期连接指连接意味着方法调用直到运行时才被实现;
Variant不是对象指针;对象的调用方法是后期连接;
编写快速、简单的客户应用程序不用费力去输入一个类型库;
派遣接口(DispInterface)
在接口和Variant中间的某个地方就是派遣接口;与接口有很多类似;
只是方便客户而设定的;
并没有在服务器上实现派遣接口;是服务器上实现了接口;
假设服务器的COM对象也支持IDispatch接口,客户应用程序可以使用Variants或派遣接口;
双重接口(Dual
Interface): 自动化服务器
简单定义为自动化服务器,支持早期连接(接口)和后期连接(Variants);
使用delphi创建的任何任何自动化服务器将自动支持双重接口;
4.2 进程内自动化服务器
目的: 创建并使用进程内自动化服务器;
CreateOleObject和GetActiveOleObject
CreateOleObject:总是创建特定服务器的新实例;
GetActiveOleObject: 用来获取正在内存中运行的服务器的引用;
例如:
Procedure StartOrLinkToWord;
var v: Variant;
begin
try
v := GetActiveOleObject('Word.Basic'); //判断是否Wod已启动;
except
v := CreateOleObject('Word.Basic'); //否则启动Word;
通常自动化服务器启动是隐藏的;
v.AppShow; //显示Word应用程序;
end;
V.FileNew; //创建一个新的文档;
v.Insert('Automation is easy!');
end;
访问自动化服务器的方法:
Interface 通过接口;
Variant 通过Variant: 通过可使用的类名;CreateOleObject
DispInterface 通过派遣接口;
4.3 进程外自动化服务器
目的:
自动化的基础;创建一个Automation服务器,如何使用接口、派遣接口、和Variants来控制此服务器;
在本身应作为独立服务器应用程序的情况下,使用进程外非常好;
HResult:
自动服务器中的所有方法必须返回一个HResult,暗示是成功还是失败;
所有其他参数必须在out参数中返回;
safecall:
调用协议可使编码;
指示Delphi自动把所有方法包括在try...except模块中;
在客户端safecall导致客户检查是否有HResult类型的返回失败码;
4.4 COM事件和回调
目的: 两种方式的比较;
派遣接口(DispInterface):
与Interface的比较
DispInterface: 为事件提供了更好的支持;与VB兼容;实现事件是最兼容的方法;
Interface: 不与VB兼容;但速度比派遣接口稍快;
回调接口(CallBack
Interface):除非有极重要的原因否则不要使用;
服务器做大量的工作,而刻户端不需要太多的工作;
接口中定义回调方法,而不是使用派遣接口把事件送回到客户端;
回调接口在服务器中定义,但在客户中实现;
如果知道服务器和客户提供的接口,回调接口更有效;
4.5 自动化ADO: Microsoft最新的数据库技术;
目的:如何使用Delphi来快速、轻松的使用基于COM的技术;
主要组件:Connection、RecordSet、Command;
Connection对象: 用于连接到一个本地或远处的数据库;
RecordSet对象: 提供了一个记录集的连接;
Command对象: 向数据库发出命令,不返回结果集;
连接数据库:Connection.Open();
记录集打开:RecordSet.Open();
执行命令:对数据库进行操作,并返回结果集,应使用Command对象(与Parameter一起工作);
访问字段值:
访问Field对象的值: RecordSet.Fields.Item[FieldNo].Value;
访问字段名称:RecordSet.Fields.Item[FieldNo].Name;
数据库错误处理:Error对象,该对象提供任何错误的详细代码;
4.6 小结
目的: 接口、派遣接口和Variants三种方法之间的不同,在特定的环境中使用那种方法;
创建进程内和进程外COM自动化服务器;
Vraiants在COM自动化服务器中的作用,以及如何使用容许后期连接到自动化对象;
实现COM事件和回调;
如何使用Delphi能相对轻松的使用ADO;
知道接口、派遣接口、和Variants的区别;
[第一章 接口][第二章 接口与COM][第三章 类型库][第四章自动化][第五章 ActiveX][末尾]
第五章
ActiveX控件和ActiveForms
5.1 ActiveX控件
目的:
5.2
创建ActiveX控件;
目的:
5.3
ActiveForm(Active窗体);
目的: 组件导向的中介软件;ActiveForm的开发;开发浏览器的客户端应用程序;
创建ActiveX工程:
New-->ActiveX-->ActiveX Library
New-->AcitveX-->Active Form
设计时期开发执照;
关于对话框;
版本控制编号;(一般要包括,控制新旧版本、以及是否从服务器下载);
分发:Project-->Web Deploy Options
Target dir: ActiveForm组件于HTML首页中codebase tag的目的;
Target URL: Delphi分发ActiveX组件的时候自动产生一个HTML的首页档案;
HTML dir: 储存这个HTML首页档案的位置;
Use CAB file compression:
Include file version number: 分发时包含版本信息;
Auto increment release number: 自动增加版本编号;
Deploy required package:
Deploy additional file:
project-->Wep deploy开始分发;
5.4 小结
目的:加强ActiveX组件的能力;
使用ActiveX组件;
如何把已有组件转换为ActiveX组件;实现Delphi没有提供的功能;
如何从窗体创建ActiveForm;
//下面是CSDN上另外一个博客里的接口
delphi 接口 转http://blog.csdn.net/lailai186/article/details/7390397
习 delphi 接口 一切都是纸老虎!!!
第四章
前不久,有位搞软件的朋友给我出了个谜语。谜面是“相亲”,让我猜一软件术语。我大约想了一分钟,猜
出谜底是“面向对象”。我觉得挺有趣,灵机一动想了一个谜语回敬他。谜面是“吻”,也让他猜一软件术
语。一分钟之后,他风趣地说:“你在面向你美丽的对象时,当然忍不住要和她接口!”。我们同时哈哈大
笑起来。谈笑间,似乎我们与自己的程序之间的感情又深了一层。对我们来说,软件就是生活。
第一节
“接口”一词的含义太广泛,容易引起误解。我们在这里所说的接口,不是讨论程序模块化设计中的程序接
口,更不是计算机硬件设备之间的接口。现在要说的接口,是一种类似于类的程序语言概念,也是实现分布
式对象软件的基础技术。
在DELPHI中,接口有着象类一样的定义方式,但不是用保留字class,而是用interface。虽然接口和类有着
相似的定义方式,但其概念的内涵却有很大的不同。
我们知道,类是对具有相同属性和行为的对象的抽象描述。类的描述是针对现实世界中的对象的。而接口不
描述对象,只描述行为。接口是针对行为方法的描述,而不管他实现这种行为方法的是对象还是别的什么东
西。因此,接口和类的出发点是不一样的,是在不同的角度看问题。
可以说,接口是在跨进程或分布式程序设计技术发展中,产生的一种纯技术的概念。类的概念是一种具有普
遍性的思想方法,是面向对象思想的核心。但是,接口概念也的确是伴随面向对象的软件思想发展起来的。
用接口的概念去理解和构造跨进程或分布式的软件结构,比起早期直接使用的远过程调用(RPC)等低级概念
更直观和简单。因为可以象理解一个对象一样理解一个接口,而不再关心这个对象是本地的或远程的。
在DELPHI中,接口被声明为interface。其命名原则是:接口都以字母I开头命名,正如类都以字母T开头一样
。在接口的声明中只能定义方法,而不能定义数据成员。因为,接口只是对方法和行为的描述,不存储对象
的属性状态。尽管在DELPHI中可以为接口定义属性,但这些属性必须是基于方法来存取的。
所有的接口都是直接或间接地从IUnknown 继承的。IUnknown是所有接口类型的原始祖先,有着类概念中
TObject的相同地位。“一个接口继承另一个接口”的说法其实是不对的,而因该说“一个接口扩充了另一个
接口”。接口的扩充体现的是一种“兼容性”,这种“兼容”是单一的,绝不会存在一个接口同时兼容两个
父接口的情况。
由于接口只描述了一组方法和行为,而实现这些方法和行为必须靠类。接口是不能创建实例的,根本就不存
在接口实例之说,只有类才能创建对象实例。但一个接口的背后一定会有一个对象实例,这个对象就是接口
方法的实现者,而接口是该对象一组方法的引用。
从概念上讲,一个对象的类可以实现一个或多个接口。类对接口的责任只是实现接口,而不应该说类继承了
一个或多个接口。“实现”一词和“继承”一词有不同的含义,应该从概念上区分开来。
一般情况下,声明接口时需要一个能唯一标识该接口类型的GUID标识符。接口类型是要被分布在不同进程空
间或计算机上的程序使用的,不象类的类型只是在一个程序空间内标识和使用。为了保证一种接口类型在任
何地方都能被唯一识别,就必须要一种有效标识不同接口的方法。用人工命名的方法是不行的,没有谁能保
证你开发的接口不会与别人重名。于是,一种所谓“全球唯一标识符”GUID(Globally Unique Identifier
)应运而生。它是通过一种复杂的算法随机产生的标识符,有16个字节长,可以保证全世界任和地方产生的
标识是不同的。在DELPHI的编辑环境中,你可以用Ctrl+Shift+G轻松产生一个GUID标识符,来作为接口的唯
一标识。
为接口指定GUID是必要的。虽然,不指定接口的GUID也可以常常可以编译通过,但在使用一些与接口识别和
转换相关的功能时一定会有问题。特别是在基于COM的程序开发中,GUID一定不可少。
接口的概念其实很简单,但却在分布式软件开发中起了关键作用。有的朋友之所以认为接口比较复杂,主要
是因为不了解接口的概念和原理。因为人们总是对自己未知的东西有一种神秘感。这种神秘感往往会使人对
未知世界产生畏惧心理。要揭开接口的神秘面纱,就必须不断的去学习和理解接口的奥秘。其实,在探索的
过程中还会有许多的乐趣,你说对吧。
第二节
因为IUnknown是所有接口的共同祖先,所以一定要首先了解它。知道事情的起因,可以有效地帮助我们理解
事情的过程和结果。IUnknown的原始定义是在System.pas单元中。因为定义在System.pas单元,那么一定是
与系统或编译器相关的原始东西。一看IUnknown的定义,很简单,一共才6行。
不过,这6行定义代码可是接口世界的基础。其中的三个接口方法蕴涵着简单而又博大精深的哲理,理解这些
哲理将使我们在编写基于接口的程序时受益非浅。
IUnknown的这三个接口方法是每一个接口对象类必须实现的方法,是接口机制的基础方法。为什么说这三个
方法是接口机制的基础?且听我漫漫道来。
首先来谈谈QueryInterface接口方法。我们知道一个对象类是可以实现多个接口的。任何接口对象都一定实
现了IUnknown接口。因此,只要你获得了一个接口指针,那一定可以通过这个接口指针调用QueryInterface
方法。而调用QueryInterface就可以知道这个接口指针还实现了一些什么接口。这对接口编程机制来说非常
重要。判断一个接口指针是否实现了某接口功能,不同接口类型之间的接口匹配和转换,都与
QueryInterface方法相关。
QueryInterface有两个参数和一个返回值。第一个参数是接口类型的标识,即一个16字节的GUID标识。由于
DELPHI编译器知道每种接口对应什么GUID,所以你可以直接使用象ImyInterface之类的标识符作为第一个参
数。如果,该接口支持第一个参数指定的接口类型,则将获得的接口指针通过第二个参数Obj送回给调用程序
,同时返回值为S_OK。
从这里也可以看出,为什么为接口指定GUID标识是必要的。因为,QueryInterface方法需要这样的标识,而
它又是接口和匹配和转换机制的基础。
接下来我们再谈谈_AddRef和_Release极口方法。_AddRef和_Release接口方法是每一种要接口对象类必须实
现的方法。_AddRef是增加对该接口对象的引用计数,而_Release是减少对接口对象的引用。如果接口对象的
引用计数为零,则要消灭该接口对象并释放空间。这本是接口机制要求的一个基本原则,就好象1+1=2这样简
单的道理,不需要深奥的解释。数学家才会有兴趣去研究一加一为什么会等于二?但数学家对1+1=2的理解是
透彻的。同样,对接口对象引用机制的深刻理解,会让我们明白许多道理,这些道理将为我们的开发工作带
来益处。
有位大师曾说过:接口是被计数的引用!
要理解这句话,我们首先要理解“引用”的概念。“引用”有“借用”的意思,表明一种参考关系。引用的
一方只存在找到被引用一方的联系,而被引用的一方才是真正的中心。由于,通过这种引用关系可以找到对
象,因此,引用实际就是该对象的身份代表。在程序设计中,引用实际上是一种指针,是用对象的地址作为
对象的身份代表。
在不是基于接口机制的程序中,本不需要就对象的引用关系进行管理。因为,非接口对象的实例都在同一个
进程空间中,是可以用程序严格控制对象的建立、使用和释放过程的。可是,在基于接口机制的程序中,对
象的建立、使用和释放可能出现在同一进程空间中,也可能出现在不同的进程空间,甚至是Internet上相隔
千里的两台计算机中。在一个地方建立一个接口,可能实现这个接口的对象又存在于另一个地方;一个接口
在一个地方建立后,又可能会在另一个地方被使用。在这种情况下,要想使用传统的程序来控制对象的建立
和释放就显得非常困难。必须要又一种约定的机制来处理对象的建立和释放。因此,这一重任就落到了
IUnknown的_AddRef和_Release的身上。
这种接口对象引用机制要求,接口对象的建立和释放由对象实例所在的程序负责,也就是由实现接口的对象
类负责。任何地方引用该对象的接口时,必须调用接口的_AddRef方法。不再引用该对象时,也必须调用接口
的_Release方法。对象实例一旦发现再也没有被任何地方引用时,就释放自己。
正是为了解决接口对象实例空间管理的问题,_AddRef和_Release方法才成为所有接口对象类必须实现的方法
。
第三节
初看本节的标题似乎有点吓人。接口对象怎么会和生与死联系起来呢?接口对象的生死真的那么重要吗?一
个好的统治者应该关心百姓的生死,同样,一个好的程序员也应该关心对象的生死。而接口对象又是流浪在
分布式网络中的游子,我们更应该关心他们的生死!
由于,接口对象是伴随接口引用的产生而建立,又伴随接口引用的完结而消亡。在DELPHI 中使用接口,似乎
没有人关心,实现接口的对象是怎样出身又怎样死亡的。这正是DELPHI中使用接口的简单性,也是其在解决
接口机制的使用问题上所追求的目标。需要接口时总有一个对象会为她而生,一旦不再引用任何接口时这个
对象又无怨无艾的死去,绝不拖累系统一个字节的资源。真有点“春蚕到死丝方尽,蜡炬成灰泪始干”的凄
情。
因为接口对象的生死直接和引用该对象的接口数目有关,所以研究在什么情况下会增加一次接口引用,又在
什么情况下会减少一次接口引用,是了解接口对象生死的关键。
现在我们来实现一个最简单的接口对象类TIntfObj,它只实现了IUnknown接口中定义的三个基本方法。有的
朋友一看就知道,这个类实际抄袭了DELPHI中TInterfacedObject类的部分代码。只是我们分别在_AddRef和
_Release方法中增加了一些信息输出语句,以便于我们探索接口对象的生死问题。请看下面的程序:
program
ProgramA;
uses
type
function
TIntfObj.QueryInterface(const IID: TGUID; out Obj): HResult;
stdcall;
const
begin
end;
function TIntfObj._AddRef: Integer;
stdcall;
begin
end;
function TIntfObj._Release:
Integer; stdcall;
begin
end;
var
procedure
IntfObjLife;
begin
end;
begin
end.
我们需要用单步调试功能来研究接口引用计数的增减与接口生死的关系。因此,建议你将Options选项中
Complier页的Optimization项清除,以避免编译器会优化掉我们需要的指令。
当程序执行到IntfObjLife子程序的三行代码时,请一步一步的调试代码。你会发现,当发生一次对接口类型
变量的赋值行为时,就会引发对接口引用计数的增减。
执行语句
aInterface:=aObject;
就会出现“Reference count increase to
1.”的信息,表明增加了一次接口引用。
而执行语句
aInterface:=nil;
就会出现“Reference count decrease to 0, and destroy the object.”,表明接口引用减少到零并且删
除了接口对象。
所以,我们可以得出结论:当将引用值赋值给接口类型的变量时,会增加对接口对象的引用计数;而当清除
接口类型变量的引用值时(赋nil),就会减少对接口对象的引用计数。
在看看下面的代码,加深一下对这一结论的理解。
var
……
aObject :=
TIntfObj.Create;
InterfaceA := aObject;
//引用增加至1
InterfaceA := InterfaceA;
//引用增加至2,但立即又减少至1
InterfaceB := InterfaceA;
//引用增加至2
InterfaceA := nil;
//引用减少至1
InterfaceB := InterfaceA;
//引用减少至0,释放对象
……
无论是将接口对象赋值给变量,还是将接口变量赋值给接口变量,以及将nil赋值给接口变量,都印证这一结
论。有趣的是,其中的InterfaceA := InterfaceA一句执行时,接口对象的引用先增加然后立即减少。为什
么会这样呢?留给你自己去思考吧!
接着,我们再来看看下面的代码:
procedure
IntfObjLife;
var
begin
end;
这个过程与前面那个过程不同的是,将变量定义为局部变量,并且最后没有给接口变量赋nil值。单步调试这
段代码我们发现,在程序运行到子程序end语句之前,接口对象的引用还是减少为0并被释放。这是为什么呢
?
我们知道,变量是有作用域的。全局变量的作用域是程序的任何地方,而局部变量的作用域只是在相应的子
程序内。一旦变量离开它的作用域,变量本身已经不存在,它存储的值更是失去意义了。所以,当程序即将
离开子程序时,局部变量aInterface将不在存在,而其所存储的接口对象引用值也将失去意义。聪明的
DELPHI自动减少对接口对象的引用计数,以确保程序在层层调用和返回中都能正确地管理接口对象的内存空
间。
因此,我们又可以得出新的结论:当任何接口变量超出其作用域范围的时候,都会减少相关接口对象的引用
计数。
要注意的是,子程序的参数变量也是一种变量,它的作用域也是在该子程序范围内。调用一个含有接口类型
参数的子程序时,由于参数的传递,相关接口对象的引用计数会被增加,子程序返回时又被减少。
同样,如果子程序的反回值是接口类型时,返回值的作用域是从主调程序的返回点开始,直到主调程序最后
的end语句之前的范围。这种情况同样会引起对接口对象引用计数的增减。
该总结一下对象的生与死了。盖棺论定后我们可以得出以下原则:
1. 将接口对象的引用值赋值给全局变量、局部变量、参数变量和返回值等元素时,一定会增加接口对象的
引用计数。
2. 变量原来存储的接口引用值被更改之前,将减少其关联对象的引用计数。将nil赋值给变量是一个赋值和
修改接口引用的特例,它只减少原来接口对象引用计数,不涉及新接口引用。
3. 存储接口引用值的全局变量、局部变量、参数变量和返回值等元素,超出其作用域范围时,将自动减少
接口对象的引用计数。
4. 接口对象的引用计数为零时,自动释放接口对象的内存空间。(在一些采用了对象缓存技术的中间件系
统中,如MTS,可能并不遵循这一原则)
需要提醒你的是,一旦你将建立的接口对象交给接口,对象的生死就托付给接口了。就好象将宝贝女儿嫁给
忠厚的男人一样,你应该完全信任他,相信他能照顾好她。从此,与对象的联系都要通过接口,而不要直接
与对象打交道。要知道,绕过女婿直接干预女儿的事情有可能会出大问题的。不信,我们看看下面的代码:
program
HusbandOfWife;
type
constructor
TWife.Create(Something:string);
begin
end;
function
TWife.GetSomething:string;
begin
end;
procedure
HusbandDoing(aHusband:IHusband);
begin
end;
var
begin
end.
请大家仔细看最后面begin至end之间的代码。我尽量将程序写的有趣和易于理解,希望你能读懂我的程序。
其基本意思是,产生一个TheWife对象之后,一旦将该对象传递给一个IHusband类型的接口后,再使用
TheWife直接操纵对象就可能发生想象不到的问题!那“万贯家财”给出去容易,拿回来可就难哟。
所以,在进行基于接口的程序设计中,请切记:接口对象一旦建立,请永远用接口来操纵对象!
现在,你该认为本节的题目并不吓人了吧!如果是这样,我写本节的目的就达到了。我也因为你能静静地倾
听我诉说衷肠而感到心情舒畅。那么,谢谢你!让我们共同在愉快的心情中继续前进吧。
第四节
俗话说,每一个成功的男人背后,一定有一个了不起的女人。同样,每一个接口背后一定有一个了不起的对
象。今天我们将了解一下接口是如何映射到对象而实现接口功能
的。
我们都知道,一个接口值实际上是一个指针。这个指针指向一张方法地址表,其中地址表的每一项存有真正
要调用的对象方法的地址。通过接口值调用方法时,就可以通过其指向的方法地址表中映射的对象方法地址
找到对象的方法,从而实现正确的方法调用。
这样理解接口到对象的方法映射关系本没有什么不正确,但却忽略了怎样通过接口只找到其引用的对象的问
题。因为,一个接口值不仅代表了实现的一组方法,而且还表明这个接口值的方法是由其引用的那个对象实
现的,而不是由另一个对象实现的(尽管是同一类的对象)。
所以,对接口值的调用首先是定位对象实例,然后才能调用对象的方法。
在我们思考问题的时候,常常会犯一些习惯性错误。又一次,我找一个朋友借车。我拿着车钥匙就去车库了
。到了车库我才发现那里停有许多的车,于是不得不打在电话询问。因为,我急于开车而忘了问是什么车。
同样,许多朋友在讨论接口到对象的映射关系时,也往往忽略了对象本身的映射问题。
其实,接口指针所对应的方法表中并不是直接存储的对象方法地址,而存储的是实现每一个接口方法到对象
方法跳转的一小段代码的地址。这一小段代码首先将接口指针转换为对象指针,然后才跳转到对象的方法地
址,调用其值向对象的方法。
在DELPHI中,一个接口对象的实例空间中存有该对象所实现的所有接口的方法表指针。如果将对象实例空间
中的接口方法表指针看作一个对象的数据成员,则该数据的地址值就是一个接口引用值,即接口指针。实际
上,接口方法表指针就是一个接口对象的数据成员,它在对象实例空间中的偏移是固定的。因此,DELPHI只
需要将接口指针减去这个固定偏移就可得到对象的指针。接口方法到对象方法跳转的那小段代码,就是实现
了这种映射。
总之,接口就是虚方法地址表的说法并不完全正确,接口的实现有更多要考虑的东西。但这并不影响我们使
用DELPHI开发复杂的应用程序。这只是DELPHI内部实现接口机制的奥秘,应该由缔造DELPHI的大师们去考虑
。我们还是来关心一下程序语言一级的接口方法映射问题吧。
在默认的情况下,我们都将接口类的方法与所实现接口类型的方法取成相同的名称,就好像重载类的虚函数
一样。但接口类的方法与接口类型的方法之间不是重载关系,而是对应关系。没有任何人说过接口接口类的
方法的名称一定要与接口类型的方法相同,只是这种相同的名称可以让DELPHI编译器自动地对应接口和类的
方法。
假设我们定义了一个IMailBox的接口如下:
我们用下面的TMailBox类来实现这一接口:
由于这个TMailBox的方法名称与IMailBox的方法名称完全相同,所以DELPHI编译器会自动将接口的方法映射
到相关的类对象的方法上。
但如果我们改用下面的TMailBox类实现这一接口:
这时,编译器无法在TMailBox的定义中找到一个与IMailBox的PutMail方法相匹配的定义,将提示
“Undeclared identifier: 'PutMail'”。其实,我们的意思是要用TMailBox的SetMail方法实现IMailBox接
口的PutMail方法,只是它们的名称不同。但编译器不知道,它还没有聪明到理解自然语言的地步。
这时候,我们需要用到接口方法映射语句。重新定义TMailBox如下:
其中的procedure IMailBox.PutMail = SetMail;一句,是告诉编译器:IMailBox的PutMail要映射到类的
SetMail方法上。这样,程序就可以正常编译通过了。
这再次说明,对象类与接口不是继承关系,对象类方法与接口方法也不是虚函数的重载关系。而是对象类实
现了接口,接口方法被映射到对象方法。
你可以将定义对象类想象为设计一块电路板。如果,你当初就是按照某总接口标准来设计电路板的输入和输
出引脚,那么电路板设计好后肯定可以直接连接到标准接口上。否则,就要再焊接一些跳线,以实现接口引
脚到电路板引脚的映射。
接口概念的好处就是,使用接口的人也许永远不知道接口功能是怎样实现的,但这种功能却应用得很好。而
接口的使用者就可采用尽可能灵活的方式实现接口功能,不用担心外人会窃取你的实现技术机密。
在DELPHI中一个对象类实现接口时,它可以将这种实现要求委托给另外一个对象或接口来完成。这使得接口
功能的提供者有了一种更灵活的接口实现方法。
我们来看看下面的程序:
program
ServiceCenter;
type
procedure
TWasher.WashClothing;
begin
end;
procedure
TRemover.MoveHouse;
begin
end;
constructor
TServiceCenter.Create;
begin
end;
function
TServiceCenter.FindRemover:IRemover;
begin
end;
begin
end.
在这个程序中我们分别定义了洗衣工和搬运工的接口IWasher和IRemover,以及它们的实现类TWasher和
TRemover。我们又定义了一个服务中心的类TServiceCenter,它实现IWasher和IRemover的接口。服务中心是
可以提供洗衣和搬家的服务的,但它并不自己去洗衣和搬家,而是委托给其他洗衣工和搬运工。其中的两条
定义语句:
完成这种服务的委托关系。
服务中心对象可以被任何需要IWasher和IRemover的接口引用,但它只是一个中间商,具体的工作是由
TWasher和TRemover来完成的。
其中,TServiceCenter在创建的时候建立一个TWasher对象来实现IWasher接口,为客户提供洗衣服务。因为
洗衣的服务是经常性的服务,有必要在服务中心开张的时候就雇佣洗衣工人。但是在需要提供IRemover接口
的时候,它却动态建立一个TRemover对象并返回接口。因为搬家不是经常的,在需要的时候临时雇一个搬运
工来干活就行了。
总之,接口功能委托实现是很灵活的,而且可以是动态的。了解这些奥秘,你就能编写出更好的基于接口的
程序。不过程序应该贴近和反映生活中的实际事物,这才叫面向对象。
本节的最后卖两个关子留给大家思考:
1. 在TServiceCenter的Create构造函数中建立了FWasher对象,有必要在相应的析构函数Destroy中释放吗
?
2.
是否有必要对TServiceCenter临时建立的TRemover的对象进行内存管理呢?
相信你能正确分析和解答这两个问题。
接口
接口定义了能够与一个对象进行交互操作的一组过程和函数。对一个接口进行定义包含两个方面的内容,一
方面是实现这个接口,另一方面是定义接口的客户。一个类能实现多个接口,即提供多个让客户用来控制对
象的“表现方式”。
口的函数和过程是支持这个接口的类的工作。
IUnknown在system单元中定义如下:
IDispatch =
interface(IUnknown)
DispIDs: Pointer): HResult;
stdcall;
Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult; stdcall;
end;
,它对于每一个接口来说是不同的。对IUnknown的定义来自于Microsoft的组件对象模型(COM)规范。
新的接口称为IFoo,它包含一个被称为F1()的方法:
type
type
2.实现接口
type
function TFooBar.F1 :
Interger;
begin
end;
function TFooBar.F2 :
Interger;
begin
end;
口中的方法与实现接口的类中的方法对应起来,如果一个类只是声明要实现某个接口,但并没有具体实现这
个接口的方法,编译将出错。
面的程序示例:
type
type
TFooBar = class(TInterfacedObject,
IFoo, IBar)
end;
function TFooBar.FooF1 :
Interger;
begin
end;
function TFooBar.BarF1 :
Interger;
begin
end;
技术有时又被称为委托实现,关于implements指示符的用法,请看下面的代码:
type
一个类,它包含IFoo方法或类型是IFoo的接口或IFoo派生接口。implements指示符后面可以列出几个接口,
彼此用逗号隔开。
(Aggregation)是COM中的概念。它的作用是把多个类合在一起共同完成一个任务。其次,它能够延后占用实
现接口所需的资源,直到确实需要资源。例如,假设实现一个接口需要分配一个1MB的位图,但这个接口很少
用到。因此,可能平时你不想实现这个接口,因为它太耗费资源了,用implements指示符后,可以只在属性
被访问时才创建一个类来实现接口。
存期自管理类型的,这意味着,它通常被初始化为nil,它是引用计数的,当获得一个接口时自动增加一个引
用计数;当它离开作用域或赋值为nil时它被自动释放。下面的代码演示了一个接口变量的生存期自管理机制
。
var
begin
end;
合法的:
procedure Test(FB : TFooBar)
var
begin
var
begin