Delphi 的接口机制——接口操作的编译器实现过程(1)
学习COM编程技术也快有半个月了,这期间看了很多资料和别人的程序源码,也尝试了用delphi、C++、C#编写COM程序,个人感觉Delphi是最好上手的。C++的模版生成的代码太过复杂繁琐,大量使用编译宏替代函数代码,让初学者知其然而不知其所以然;C#封装过度,COM编程注定是要与操作系统频繁打交道的,需要调用大量API函数和使用大量系统预定义的常量与类型(结构体),这些在C#中都需手工声明,不够简便;Delphi就简单多了,通过模版创建的工程代码关系结构非常清晰,而且其能非常容易使用API函数和系统预定义的常量和类型(只需引用先关单元即可),但在使用过程中也发现了一些缺点。【注1】
(1)有些类型(结构体)的成员类型与C++中的不是等效对应关系,如SHFileOperation函数的参数类型是SHFILEOPSTRUCT结构体,delphi中它的两个路径成员被定义成PWideChar型,与C++的LPCTSTR不一致,PWideChar是以空字符(\0)结尾的,致使这两个成员不能包含多个文件路径。【注2】
(2)有些接口的函数参数定义不一致,如IContextMenu.InvokeCommand函数参数在Delphi中是CMINVOKECOMMANDINFO类型,在c++中是LPCMINVOKECOMMANDINFO型 ,致使该接口函数不能使用扩展的CMINVOKECOMMANDINFOEX型参数。【注3】
Delphi操作COM的另一便处在于他的接口的引用计数管理,这为我们写程序解决了一大麻烦:不用管接口的AddRef和Release了,直接把接口当“接口指针变量”(【注4】)使用,编译器会执行一些特殊的代码自动维护接口的引用计数。当然,这也会带来另一个问题,接口相当于“变量”一样使用,这就涉及到“变量”的生命周期问题,当把这样一个局部“变量”通过强制类型转换(【注5】)给一个全局变量时,待之后转换回来时将引发错误。因为局部“变量”生命已结束,要被清理,其所代表的接口被减少引用计数释放了,如果人为让“变量”AddRef一次,就能消除这个错误。
关于Delphi的接口引用计数管理,在网上看到的一篇介绍的文章,查很久了它的出处,目前已知最早是SaveTime于2004年2月3日发表于大富翁论坛。【注6】
下面将它整理了一下,以便加深对delphi对接口引用计数的理解。
接口指针变量赋值
接口是生存期自管理对象,即使是局部接口指针变量,也总是被初始化为 nil。接口被初始化为nil是很重要的,从下文中Delphi生成维护接口引用计数的代码时可以看到这一点。
- var
- MyObject: TMyObject;
- MyIntf, MyIntf2: IInterface;
- begin
- MyObject := TMyObject.Create; // 创建 TMyObject 对象
- MyIntf := MyObject; // 将接口指向 MyObject 对象
- MyIntf2 := MyIntf; // 接口指针的赋值
- end;
当接口与一个对象连接时,编译器会执行一些特殊的代码维护接口对象的引用计数。例如以上代码,当执行到MyIntf :=MyObject 语句时,编译器的实现是:
1. 如果 MyObject <> nil,则设置一临时接口指针 P 指向 MyObject 对象内存空间中的“接口跳转表”指针(后面会分析“接口跳转表”),否则 P := nil;
2. 执行 System.pas 中的 _IntfCopy(MyIntf, P) 操作,进行引用计数管理。
- procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);
- var
- P: Pointer;
- begin
- P := Pointer(Dest);
- if Source <> nil then
- Source._AddRef;
- Pointer(Dest) := Pointer(Source);
- if P <> nil then
- IInterface(P)._Release;
- end;
函数_IntfCopy 的代码比较简单,就是增加 Source 接口对象的引用计数,减少被赋值的接口对象的引用计数,最后把源接口赋值至目标接口。
对于两个接口的赋值的情况,如MyIntf2 := MyIntf,这时比 MyIntf := MyObject 的情况要简单一些,编译器不需要进行对象到接口的转换工作,这时真正执行的代码是:_IntfCopy(MyIntf2, MyIntf)。
接口指针变量的清除工作
在一个过程(procedure/function)执行结束时,编译器会生成代码减少接口指针变量的引用计数。编译器使用接口指针为参数调用 _IntfClear 函数,_IntfClear 函数的作用是减少接口对象的引用计数并设置接口为 nil :
- function _IntfClear(var Dest: IInterface): Pointer;
- var
- P:Pointer;
- begin
- Result := @Dest;
- if Dest <> nil then
- begin
- P := Pointer(Dest);
- Pointer(Dest) := nil;
- IInterface(P)._Release;
- end;
- end;
通过对以上代码及分析,我们可以总结过程(procedure/function)中的接口引用计数使用规则:
1. 一般不需要使用 _AddRef/_Release 函数设置接口引用计数;
2. 可以将接口赋值为接口或对象,Delphi 自动处理源/目标接口对象的引用计数;
3. 如果要提前释放接口对象,可以设置接口指针为 nil,但不要调用 _Release。因为 _Release 不会把接口指针变量设置为 nil,最后 Delphi 自动调用 _IntfClear时会出错。
对于全局接口指针变量,在接口指针变量被赋值时增加对象的引用计数,在程序退出之前编译器自动调用 _IntfClear 函数减少引用计数以清除对象。
接口指针作为参数
1. 以var 或const 方式传递接口指针时,像普通的参数传递一样。
2. 以out 方式传递接口指针时,编译器会先调用_IntfClear 函数减少引用计数,清除接口指针为 nil 。(out 也是以引用方式传送参数)。
3. 以传值方式传递接口指针时,编译器会在参数被使用之前调用_IntfAddRef 函数增加引用计数,在过程结束之前调用_IntfClear 函数减少引用计数。
- { System.pas }
- procedure _IntfAddRef(const Dest: IInterface);
- begin
- if Dest <> nil then Dest._AddRef;
- end;
为什么以传值方式要特别处理引用计数呢?因为复制了接口指针。
1 我用的是Delphi2010,更新的XE、XE2版本可能已更正了这些问题,在此举例说明而已。
2 有关结构体SHFILEOPSTRUCT及其两个路径成员的详细介绍请参见http://blog.csdn.net/tht2009/article/details/6753706和http://msdn.microsoft.com/en-us/library/bb759795(VS.85).aspx。
3 有关接口函数InvokeCommand的详细介绍请参见http://msdn.microsoft.com/en-us/library/bb776096(VS.85).aspx。
4 我也不知严格上能否这样称呼,姑且这样类比吧!
5 如通过Pointer(IShellFolder)将一个局部声明的IShellFolder接口保存到一个Pointer型的变量Data中,通过Data:=Pointer(IShellFolder)不会增加IShellFolder接口对象的引用。实际中很少遇到这种情况,我也是在无意中发现这个问题的。
6 请见http://blog.csdn.net/huangsn10/article/details/6112546,由于大富翁论坛好像已关闭了,所以真正出处已无从考证。
http://blog.csdn.net/tht2009/article/details/6767435
Delphi 的接口机制——接口操作的编译器实现过程(1)的更多相关文章
- Delphi 的接口机制——接口操作的编译器实现过程(2)
接口对象的内存空间 假设我们定义了如下两个接口 IIntfA 和 IIntfB,其中 ProcA 和 ProcB 将实现为静态方法,而 VirtA 和 VirtB 将以虚方法实现: IIntfA = ...
- java8中的接口与时间操作
java8中接口可以有默认方法(用default修饰,可以有多个)和静态方法了. public interface Tran { default public String getName() { r ...
- MySQL数据库学习笔记(九)----JDBC的ResultSet接口(查询操作)、PreparedStatement接口重构增删改查(含SQL注入的解释)
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- c++关于接口机制和不完全类型的小问题
都和typedef有关 一个是接口机制时用到的 就是所有用到接口的源文件只需包含简单的接口声明 接口的具体实现在其他源文件中实现 接口可以是 //interface.h typedef struct ...
- Java 8-Lambda表达式、方法引用、标准函数接口与流操作、管道操作之间的关系
1.Lambda表达式与接口之间的关系 只要Lambda表达式的声明形式与接口相一致,在很多情况下都可以替换接口.见如下代码 Thread t1 = new Thread(new Runnable() ...
- Go part 6 接口,接口排序,接口嵌套组合,接口与类型转换,接口断言
接口 接口是一种协议,比如一个汽车的协议,就应该有 “行驶”,“按喇叭”,“开远光” 等功能(方法),这就是实现汽车的协议规范,完成了汽车的协议规范,就实现了汽车的接口,然后使用接口 接口的定义:本身 ...
- C#-概念-接口:接口
ylbtech-C#-概念-接口:接口 接口(硬件类接口)是指同一计算机不同功能层之间的通信规则称为接口. 接口(软件类接口)是指对协定进行定义的引用类型.其他类型实现接口,以保证它们支持某些操作.接 ...
- Java中的集合(七)双列集合顶层接口------Map接口架构
Java中的集合(七)双列集合顶层接口------Map接口 一.Map接口的简介 通过List接口,我们知道List接口下的集合是单列集合,数据存储是单列的结构.Map接口下是一个键值对(key-v ...
- java:接口特性 接口与抽象类/普通类的区别
接口 书面定义: Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能). 在ja ...
随机推荐
- win10使用python开发工具pycharm首次安装配置
刚才在网页上写了一半,结果网页出现了意外,然后,再打开什么都没有了,说多都是泪啊,我以为博客会自动保存草稿的呢,看来是我高估了它的功能然而现在根本没有心情写了... 因为出现了意外,果断的不在网页端编 ...
- 利用C#轻松创建不规则窗体
1.准备一个不规则的位图 可以使用任意一种你喜欢的作图工具,制作一个有形状的位图,背景使用一种其他的颜色.这个颜色在编程中用得着,所以最好使用一种容易记忆的颜色.如黄色,文件名为bk.bmp 2.创建 ...
- WebResource.axd文件的配置和使用
很多ASP.NET server控件都需要另外的外部资源来实现某些功能,WebResource.axd就是将一些js,jpg,bmp等封装或叫植入到类库里面. 使用WebResource.axd需要注 ...
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"的作用
为页面添加正确的DOCTYPE 很多设计师和开发者都不知道什么是DOCTYPE,DOCTYPE有什么用.DOCTYPE是document type的简写.主要用来说明你用的XHTML或者HTML是什么 ...
- OS X 键盘快捷键
了解有关常见 OS X 键盘快捷键的信息.键盘快捷键是通过按下键盘上的组合键来调用 OS X 功能的一种方式. 若要使用键盘快捷键或按键组合,您可以同时按修饰键和字符键.例如,同时按下 Command ...
- docker网络-如何让外部网络访问容器资源
docker网络-如何让外部网络访问容器资源 安装httpd 服务: docker:/root# docker exec -it f63b2633d146 bash bash-4.1# yum ins ...
- 实用chrome插件
2015年最实用的9款chrome插件 随着14年chrome浏览器的市场超过IE浏览器,chrome凭借它强劲性能和出色的使用体验真正的登上了平民级的殿堂.今天小编就为大家推荐9款自己常用的chro ...
- android开发关于和使用本机内存、内置存储卡和外置存储卡 (转)
转自:http://www.2cto.com/kf/201304/204729.html 关于android存储器简介: android开发常常需要涉及数据缓存,这就 ...
- Aix字符集
aix 安装中文字符集 1.看到系统安装过的字符集 locale -a 2.安装 smitty-->System Environments-->Manage Language Enviro ...
- 面试题之——将文件夹下java文件写入到新的文件夹,并修改扩展名
题目:将d:/code/java文件夹下的所有.java文件复制到d:/code/java/jad文件夹下并且将原来的文件的扩展名.java改为.jad 源代码: package com.zyh.in ...