作者:韩小明 来源:CSDN博客   酷勤网收集 2007-11-10

摘要
  尝试重新审视VCL中的各个基础类,其实有点大胆。所以用“苛评”这个词来做标题,表明这完全是我的苛刻,VCL的设计是非常棒的。不过也算是我使用6年Delphi的一点回报吧。以后还会继续苛评其他类。希望大家继续关注。

在李维的《inside VCL》中详细描述了VCL中TObject的地位。是的Borland的工程师们有心将Delphi语言做成pure language。所以你几乎可以看到TObject的所有pure pascal的实现。

更重要的,你应该会发现。Delphi将代码的所有运行机制都暴露在我们面前。这也就是Delphi的TObject和C++中的Object以及C#的Object有很大不同的地方。

Delphi将整个语言的机制都在TObject上实现了。消息机制、接口机制、面向对象机制(多态)等等你都可以从TObject的实现代码中看到运行的全部流程。

不管TObject如何优秀,可是TObject正如Borland一样,在它身上总是看到受制于MS的影子。特别是接口机制。我有两点认为TObject的设计不好。

  1. 对象指针和接口指针并不是同一个地址
  2. 对象和接口不可以混用

先说说我第一个不满意的地方。说是第一,并不表示是“最”的意思。只是代表“最先”的意思。

在Delphi的TObject中,类在实现了接口之后,其创建的实例中,对象指针和接口指针并不是同一个地址。这个可能大家没有注意到,但是主要通过简单的例子(取两个指针的地址)就可以发现这个现象。

你可能不会在意这个差异。是的,其实也没什么,不是同一个地址,代码照样可以工作。在对象的内存实例模型中,接口的指针直接存储在属性后面(如果存在继承,可能会出现交错的)。因为TObject设计的时候,两个地址不在一起,那么就存在两个问题:

  • 如果从对象转换到接口
  • 如何从接口转换到对象

稍微知道对象模型的人,就应该知道对象调用虚拟方法表的过程。只有对象指针可以指向VMT。另外,在接口访问方法的实现代码的时候,也需要传入对象指针。这是为什么?我们知道,一个对象的方法中,有一个隐含的指针,我们一般称它为Self指针。只要不是class function,那么这个Self指针就是指向对象实例的指针。所以在代码的调用过程中,必然有这个转换。

TObject提供的AS操作,可以完成第1个转换,但是可惜的是,TObject并没有提供一个公开的方法来负责第2个转换。这是我认为TObject在这方面设计不好的原因。(JCL代码库中有第2个转换的代码)

第二个我不满意的地方就是TObject在生存期管理上,没有做到和接口一致。我相信这也是许多使用Delphi接口的同志们一致的想法。虽然我们现在看到Java和C#都已经做到这点,可不能不指出的是,在Delphi中,对象和接口不可以混用!

我并不是奢求TObject的生存期可以自管理。毕竟,我还是习惯了“谁创建谁释放”的规则。可是一旦到了接口和对象混合使用的时候,就发生了问题。

这虽然可以解释为接口是Delphi为了迎合COM而后加上的。我们今天却应该来设计一下一种规则,来解决接口混用的问题。我认为可以有一种简单的方式:TObject在Free的时候,发现其接口引用计数不为0的时候,不会Destroy。

目前我们无法做到这点,这是因为Destroy是Delphi默认做的,也就是说,只要调用了Free方法,Destroy必然发生。我们无法完美地改变这个现实。也正因为此,我才认为必须在设计TObject的时候,将这个考虑进去!

OK。尝试重新审视VCL中的各个基础类,其实有点大胆。所以用“苛评”这个词来做标题,表明这完全是我的苛刻,VCL的设计是非常棒的。不过也算是我使用6年Delphi的一点回报吧。以后还会继续苛评其他类。希望大家继续关注。

来自:http://blog.csdn.net/xiammy/archive/2007/02/02/1500451.aspx

评论

  fellow99 发表于2007-02-02 10:17:47  IP: 202.116.22.*
在java/cpp中使用interface那是多么顺手多么自然的事情。换到delphi来,必须步步为营!
我承认,我对delphi的接口实现不熟。。。好吧,我不在delphi中用接口了,我放弃。

  xiammy 发表于2007-02-02 11:05:24  IP:
呵呵,要用的时候,还是得用。

  zhmnsw 发表于2007-02-02 11:37:49  IP: 218.12.29.*
无聊的比较

语言所处的层次不一样,为什么要以己之长来比别人的短处呢?
VCL的设计同时兼顾了效率和扩展性,那么有些东西是需要牺牲的,它也比不过JAVA那种“反正我就是慢,不在乎那些牺牲”的言论,语言各有特点,各有各的适用范围,要是都一样了,干脆只出一种语言好了。

.NET或JAVA里对象接口混用是建立在独立的垃圾回收机制上的,但VCL没有,不要告诉我,这一点就是VCL不如这两种语言的原因之一。

虽然楼主恨铁不成钢的心情是可以理解的,但很多理由都不够充分,甚至是狡辩,没有杀伤力,就好比拿白痴能生活自理和很多医生不是院长来比一样。

VCL是不断进步的,如果在将来,它可以放弃以前要兼顾的一些特性,就能够踢开很多限制,实现更多的特性。

  zhmnsw 发表于2007-02-02 11:41:00  IP: 218.12.29.*
如果非要深究,楼主还是跟李维老师探讨一下吧,我等小辈水平太有限了,不过别忘了把聊天记录贴上共享哦。
以上言论如有不妥,敬请不吝赐教

  ralf1999 发表于2007-02-02 11:44:39  IP: 222.131.201.*
定义一个这样接可就可以解决你说的那个两问题
IObject = interface
function GetObjectInstance: TObject;
procedure ReleaseObjectInstance;
end;

其实也没什么,DELPHI就是DELPHI,有她自己的风格,只是你还不了解。

  Rainstorey 发表于2007-02-02 12:06:01  IP: 222.92.92.*
真所谓有得必有失。世界并非完美,人也是,语言机制也一样。某一特性或许在你看来是累赘在别人看来就是完美。要用不同角度看问题,没有绝对的不好也没有决对的Perfect

  zeroyet 发表于2007-02-02 12:36:43  IP: 218.249.11.*
你要求的接口混用的情况出现,我觉得是你设计出了问题,不是接口没有设计好,就是对象没有设计好。
还有关于释放的问题你的看看TInterfacedObject类了,TInterfacedObject类中Destroy是不会把还在引用的对象释放的,只是抛出一个异常(当然你可以改变这一点),你如果要用接口就从这个类派生。
TObject是核心,他不会为一个特定的需求去设计什么,我觉得设计本应该如此。

  hacker47 发表于2007-02-02 12:47:44  IP: 61.191.117.*
用java和c++的思想来使用delphi/pascal,
那是人的问题,不是delphi的问题。

  cocoboy79 发表于2007-02-02 13:09:27  IP: 60.27.21.*
VCL就现在来说,本来也就不是什么多先进的东西了。

  SeaWave 发表于2007-02-02 16:11:09  IP: 124.161.135.*
原文中说“对象指针和接口指针并不是同一个地址”,这句话没有交代明白,同一个对象实例,怎么可能不是同一个地址呢,这一点解释不通。
事实上,做个简单的实验,发现对象指针和指口指针肯定是同一个地址。新建一个工程,在Form上放一个Memo控件:

type
IFoo = interface
procedure Execute;
end;

TFoo = class(TInterfacedObject, IFoo)
procedure Execute;
procedure MyMethod;
end;

procedure Test(AFoo: IFoo);
begin
AFoo.Execute;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
intf: IFoo;
obj: TFoo;
begin
obj := TFoo.Create;
obj.Execute;
intf := obj;
intf.Execute;
Test(obj);
Test(intf);
end;

procedure TFoo.Execute;
begin
Form1.Memo1.Lines.Add(Format('this=%d', [Integer(Self)]));
end;

结果当然是地址都相同。

这段代码还说明了从对象转换到接口是很轻松的事情,连AS都不用(上面Test过程要求一个接口,由于obj这个对象实例实现了该接口,所以直接传递就可以了)。

至于原文中所说的“如何从接口转换到对象”,首先想到的是为什么要把接口转换到对象?动机是什么?通常,编程是基于抽象,用接口的目的就是为了让上层不必了解底层细节,将派生类看生基类是很自然的做法,反过来把基类向下强制转换为派生类就很费解了。

如果非要这么做,当且仅当你知道intf实现就是TFoo时,你可以用一个强制类型转换:

TFoo(intf).MyMethod;

  ralf1999 发表于2007-02-02 17:06:03  IP: 221.220.89.*
在实际情况中,将INTERFACE转换成OBJECT也是很正常的事。你提供的方法:
TFoo(intf).MyMethod;
强制将INTERFACE转换成OBJECT,将跳过DELPHI的编译检查,如果转换错误将得到一个空指针。如果你的INTERFACE定义的层次很深的话也不能保证转换成功。正常情况下应当什么AS操作转换,但DELPHI中AS操作不能作用于INTERFACE到OBJECT的转换。

DELPHI提供一个标准方法将INTERFACE转换为对象,即IInterfaceComponentReference接口,但此方法只针对从TCOMPONENT派生的类.但对于大规模复用的设计来说,使用TCOMPONENT作为最初的基类是唯一的选择。

也可以参照IInterfaceComponentReference,定义自已的INTERFACE类型树,系统的解决这个问题。

编程就像走钢丝,如果你掉下来,那一定不是因为钢丝太细。

  maozefa 发表于2007-02-02 18:12:34  IP: 221.201.146.*
Delphi中,由对象实现的接口后,对象和接口地址确实不相同,楼上举例显示的Self地址是对象的地址而不是接口地址,只不过说明对象方法映射到接口中了,从2个方面可以证明:
1、直接显示对象和接口的地址:
ShowMessage(Format('Obj=%.8x, Intf=%.8x', [Integer(Obj), Integer(Intf)]));
可以看出它们的地址是不一样的。
2、从BCB头文件代码可以证明,以TStreamAdapter为例:
class DELPHICLASS TStreamAdapter;
class PASCALIMPLEMENTATION TStreamAdapter : public System::TInterfacedObject
{
typedef System::TInterfacedObject inherited;

private:
TStream* FStream;
TStreamOwnership FOwnership;

public:
__fastcall TStreamAdapter(TStream* Stream, TStreamOwnership Ownership);
__fastcall virtual ~TStreamAdapter(void);
(省略)
private:
void *__IStream; /* IStream */

public:
operator IStream*(void) { return (IStream*)&__IStream; }

};

当调用一个类似下面c++方法时候:
void Load(IStream *stream);
是这样传递参数的:
TStreamAdapter *adapter = new TStreamAdapter(参数省略);
Load(*adapter);
也就是说不是直接传递adapter地址,而是用了一个取值符号*,而这个取值符号又是重载了的:
operator IStream*(void) { return (IStream*)&__IStream; }
取得是TStreamAdapter的一个私有指针__IStream而已。


我上面只是实事求是指出Delphi接口和对象地址问题,并不代表我同意博主的观点,正如楼上zhmnsw所说:“我等小辈水平太有限了”。

  cuizm 发表于2007-02-02 20:46:22  IP: 60.20.193.*
路过

http://www.egooglet.com/

  xiammy 发表于2007-02-02 23:04:25  IP: 221.218.35.*
不好意思,没有交代清楚。如果大家研究过TObject的方法,就会在InitInstance方法中发现端倪。
<pre>
class function TObject.InitInstance(Instance: Pointer): TObject;
{$IFDEF PUREPASCAL}
var
IntfTable: PInterfaceTable;
ClassPtr: TClass;
I: Integer;
begin
FillChar(Instance^, InstanceSize, 0);
PInteger(Instance)^ := Integer(Self);
ClassPtr := Self;
while ClassPtr <> nil do
begin
IntfTable := ClassPtr.GetInterfaceTable;
if IntfTable <> nil then
for I := 0 to IntfTable.EntryCount-1 do
with IntfTable.Entries[I] do
begin
if VTable <> nil then
PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);
end;
ClassPtr := ClassPtr.ClassParent;
end;
Result := Instance;
end;
</Pre>
其中, PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);就是在对象实例中初始化接口的VMT指针。

  cimil 发表于2007-02-03 08:55:12  IP:
我说大哥,你起子号能不能规范点,上次那个就不说了,这个星子怎么跟日本女人似的,还是看书的时候看反了,改成子星要好多了。

  daiyun 发表于2007-02-03 12:06:10  IP: 221.137.132.*
vcl 开发出来的时候是什么年代的事情,C#开发出来的时候又是什么年代的事情,有可比性么,虽说都是出自同一人之手笔。

  yrb 发表于2007-02-03 13:09:13  IP: 219.159.20.*
哗众取宠,可恶!

  StevenLee 发表于2007-02-03 16:54:06  IP: 221.218.192.*
每种语言都有自己的优劣,取其长,避其短,这才是正道
楼主这样用Delphi,只能说明楼主对Delphi的理解过于肤浅

  aimingoo 发表于2007-02-04 00:06:15  IP: 61.173.115.*
哎~~楼上的立意就不对~~一杆子打翻一船人,激起众怒又开架~何必何必~

VCL归Delphi RTL管,COM归Win32内核管,Delphi中的Interface最早先被设计出来,是与COM有关的。你不能指望borland一开始就设计好一个与COM完全无关的接口机制,还要让它会满足“用于COM”这样一个设计需求。

又,使用COM Interface,就不要让Delphi管理实例的生存周期。混用二者是灾难。但这是COM的机制决定的,根本不是、也不需要是要Delphi通过改变对象实例的结构来解决的问题。

  xiammy 发表于2007-02-04 00:28:03  IP: 221.222.75.*
关于语言的设计,我非常赞同Ruby的最小惊讶原则。当然了,这个原则在这里我要重新诠释一下,那就是,接口的使用不应该让不了解的人走到错误的路线上。是的,我们可以约束自己的调用规范。但是,请不要忘了,我们在讨论一个好的设计,而不是好的使用技巧。

  xiammy 发表于2007-02-04 00:31:37  IP: 221.222.75.*
to aimingoo:非常感谢你的提醒,看来我的修为还是要提高提高了。

  edgethinking 发表于2007-02-05 09:15:06  IP: 58.60.128.*

没看懂. 平时工作中用不到这些知识.

  huanzhugege 发表于2007-02-05 09:26:33  IP: 61.48.59.*
正是由于Delphi对接口支持的不完善性,我逐渐放弃了他

  LastWorker 发表于2007-02-05 11:09:43  IP:
支持aimingoo的观点!
aimingoo说的其实在李维的书上说的很清楚了,楼主看来是只把注意力放到寻找"苛评"上了,忘了仔细看书了.
不过楼主"苛评"的精神是值的鼓励的.

  wr960204 发表于2007-02-05 14:24:59  IP: 61.235.75.*
大概是楼主不太知道COM的实现机制吧。有些没办法,是由COM的机制限制的,不是一个FrameWork可以搞定的。
还有接口固然是VMT,可是不能和对象的VMT混用实现接口和对象的地址一致。这个道理楼主想一下就明白了。
还有你说的As操作符的反操作,可以非常容易的实现接口到对象的转变。只要给接口添加一个GetObject的方法,在方法的实现处直接写Result:=Self;就好了

  xiammy 发表于2007-02-05 14:45:40  IP: 218.249.220.*
优美的设计必然是平衡的。正如:E=mc^2一样,E转换成质量,质量也可转换成E。
现在,提供了从对象到接口的AS,却没有从接口的对象的AS!
另外,如果Delphi不提供AS,那么你是不是就在对象里增加一个方法叫GetInterface呢?这和增加GetObject是一个道理。不在于大家是不是知道怎么实现这个功能,而在于设计本身是不是考虑好这个问题。

  mustangzhi 发表于2007-02-05 17:23:24  IP: 58.44.118.*
对象指针和接口指针并不是同一个地址

如果我没误解你的话,我觉得这点并没影响,
但你讲这个设计是个缺点的话?

我会问你对象指针和接口指针是同一地址的情况可能吗?

对象和接口的指针地址都只是引用,是两个完全不同的变量,你不可能期望一个对象的引用和一个接口的引用使用同一个地址,就像你不能期望两个不同的变量使用同一地址一样.

但接口指针和对象指针实际指向同一对象是可以的.
这种情况是实际的对象存储在同一内存空间.

如果没有另一个不同的对象引用,我们的对象又从哪里创建出来?
接口的引用本身是不能创建对象的吧?

你讲的其它几个缺点我更加是不知所云了.
(同样呀,"我等小辈水平太有限了")

  ylmg 发表于2007-02-05 23:35:06  IP:
对象指针转为接口指针,这是可以理解的,但为什么要从接口指针转为对象指针呢?如果一定要这么做,很可能是设计上出了问题。
顺便提醒一点,如果你是准备从delphi的对象模型中来找结论的话,有一个问题是无法回避的:如果这个接口的实现类不是delphi写的呢?
就算这个实现类是delphi写的,你也得考虑一些问题,例如如果是用接口委托的方式实现的话,是不是会有问题。
写过一个比较变态的函数,如下:
{
很变态的一个函数实现。
如果一个类实现了某些接口,现在已经拿到了指向接口的指针,但如果需要拿到对象指针的话,
需要用额外的比较麻烦的方法,这里提供一个函数可以方便拿到对象指针,该函数依赖于
delphi 的内部编译实现,如果这个类是由其他语言实现的话,不可以使用该函数。
因此通常情况下不建议使用该函数。
}
function GetObjectFromIntf(Intf: IUnknown): TObject;
begin
Result:= TObject(Integer(intf) - ((not PByte(PInteger(PInteger(intf)^)^ + 4)^ + 1) and $000000ff));
end;

  bluniu 发表于2007-02-06 10:51:18  IP:
看到这么多谦虚的高手,我发现我用了3年的delphi(虽然现在不用了)还基本等于白痴级。没有各位究根刨底的精神。
我发现其实大家都有个共同的,就是也同时在学习研究其它语言。。。

如果以在座各位的水平,能组织起来一起做点东西,也是件不错的事,提倡互相讨论进步,不提倡互相鄙视。

因为在delphi调用com事件响应时嫌搞得有点麻烦,而且还没实现。所以中断了学习。

顺便弱弱的文句,如果也变态的使用同一接口在单线程实现两个不同的对象(虽然一般情况下无此必要),如果接口指针与对象指针一致,那是不是说两个对象。。。,好象有点不太对劲。或者是接口VMT中也有了两处记录,com原理不懂,朋友们给解释下。


  ralf1999 发表于2007-02-06 11:31:15  IP: 221.220.91.*
关于将INTERFACE转换为OBJECT,前提是:
在不使用COM的情况下,只是将INTERFACE做为一般的OO技术使用在设计中。
例如:
由FACTORY创建的对象通常得到的是一个INTERFACE,当这个对象被交还给FACTORY消毁时需要可能需要由INTERFACE转换为OBJECT。
当使用OBJECT POOL时,当传入对象时可能需要由INTERFACE转换为OBJECT。
等等,另外一点是,VCL的设计是基于OBJECT的,当在你的设计中对VCL中的类进行包装时(包装到INTERFACE中),尢其是涉及到VISUAL OBJECT时,往往会发生INTERFACE到OBJECT的转换。

INTERFACE转为OBJECT也不是什么奇怪的事,即然存在这么一项功能,那么就可以考虑:怎么用?用在哪?为什么用?不必急着贴标签,因为技术本来就是这么一回事,没有什么不可以的。

  Myjiajia 发表于2007-02-06 12:45:36  IP: 222.70.187.*
把这篇文章放到论坛里面,你就知道自己有多肤浅了.

上面有位网友评论的好:
哗众取宠,可恶!

  lichaohui 发表于2007-02-07 20:38:12  IP: 58.240.64.*
不要有所谓受制与MS 的心理阴影,

这带来的好处就是对COM非常好的支持,
COM是一个伟大的划时代的设计,不要小看MS,
Delphi从中也得到了灵感,

在MFC中,接口地址未必就是对象地址,
因为涉及到多个虚拟方法表之间的切换,
至于混用的问题,则是明知不能而为之,
C++中的安全指针也有很多约束条件,你会故意的去违反这个
约束条件吗?

实际上,对比一下在c++ 和 delphi 中编写 COM对象和调用COm对象就会发现,TObject真是天才的设计。


  HappyTeam 发表于2007-05-15 18:02:54  IP: 211.155.28.*
各位高人,从TComponent与自定义的接口一并继承下来的类,怎样使用 接口转对象 操作?
JCL里使用什么函数?

  xiammy 发表于2007-05-15 22:41:06  IP: 221.216.18.*
参见:《JCL中由接口获得对象的方法》http://blog.csdn.net/xiammy/archive/2007/05/15/1610610.aspx

分类: Borland技术 windows开发 开发工具



关于酷勤 | 联系方式 | 免责声明 | 友情链接