作者:韩小明 来源:CSDN博客 酷勤网收集 2007-11-10
在李维的《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的设计不好。
-
对象指针和接口指针并不是同一个地址
-
对象和接口不可以混用
先说说我第一个不满意的地方。说是第一,并不表示是“最”的意思。只是代表“最先”的意思。
在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
评论
| 在java/cpp中使用interface那是多么顺手多么自然的事情。换到delphi来,必须步步为营! 我承认,我对delphi的接口实现不熟。。。好吧,我不在delphi中用接口了,我放弃。 |
| 无聊的比较 语言所处的层次不一样,为什么要以己之长来比别人的短处呢? VCL的设计同时兼顾了效率和扩展性,那么有些东西是需要牺牲的,它也比不过JAVA那种“反正我就是慢,不在乎那些牺牲”的言论,语言各有特点,各有各的适用范围,要是都一样了,干脆只出一种语言好了。 .NET或JAVA里对象接口混用是建立在独立的垃圾回收机制上的,但VCL没有,不要告诉我,这一点就是VCL不如这两种语言的原因之一。 虽然楼主恨铁不成钢的心情是可以理解的,但很多理由都不够充分,甚至是狡辩,没有杀伤力,就好比拿白痴能生活自理和很多医生不是院长来比一样。 VCL是不断进步的,如果在将来,它可以放弃以前要兼顾的一些特性,就能够踢开很多限制,实现更多的特性。 |
| 如果非要深究,楼主还是跟李维老师探讨一下吧,我等小辈水平太有限了,不过别忘了把聊天记录贴上共享哦。 以上言论如有不妥,敬请不吝赐教 |
| 定义一个这样接可就可以解决你说的那个两问题 IObject = interface function GetObjectInstance: TObject; procedure ReleaseObjectInstance; end; 其实也没什么,DELPHI就是DELPHI,有她自己的风格,只是你还不了解。 |
| 真所谓有得必有失。世界并非完美,人也是,语言机制也一样。某一特性或许在你看来是累赘在别人看来就是完美。要用不同角度看问题,没有绝对的不好也没有决对的Perfect |
| 你要求的接口混用的情况出现,我觉得是你设计出了问题,不是接口没有设计好,就是对象没有设计好。 还有关于释放的问题你的看看TInterfacedObject类了,TInterfacedObject类中Destroy是不会把还在引用的对象释放的,只是抛出一个异常(当然你可以改变这一点),你如果要用接口就从这个类派生。 TObject是核心,他不会为一个特定的需求去设计什么,我觉得设计本应该如此。 |
| 原文中说“对象指针和接口指针并不是同一个地址”,这句话没有交代明白,同一个对象实例,怎么可能不是同一个地址呢,这一点解释不通。 事实上,做个简单的实验,发现对象指针和指口指针肯定是同一个地址。新建一个工程,在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; |
| 在实际情况中,将INTERFACE转换成OBJECT也是很正常的事。你提供的方法: TFoo(intf).MyMethod; 强制将INTERFACE转换成OBJECT,将跳过DELPHI的编译检查,如果转换错误将得到一个空指针。如果你的INTERFACE定义的层次很深的话也不能保证转换成功。正常情况下应当什么AS操作转换,但DELPHI中AS操作不能作用于INTERFACE到OBJECT的转换。 DELPHI提供一个标准方法将INTERFACE转换为对象,即IInterfaceComponentReference接口,但此方法只针对从TCOMPONENT派生的类.但对于大规模复用的设计来说,使用TCOMPONENT作为最初的基类是唯一的选择。 也可以参照IInterfaceComponentReference,定义自已的INTERFACE类型树,系统的解决这个问题。 编程就像走钢丝,如果你掉下来,那一定不是因为钢丝太细。 |
| 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所说:“我等小辈水平太有限了”。 |
| 不好意思,没有交代清楚。如果大家研究过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指针。 |
| vcl 开发出来的时候是什么年代的事情,C#开发出来的时候又是什么年代的事情,有可比性么,虽说都是出自同一人之手笔。 |
| 每种语言都有自己的优劣,取其长,避其短,这才是正道 楼主这样用Delphi,只能说明楼主对Delphi的理解过于肤浅 |
| 哎~~楼上的立意就不对~~一杆子打翻一船人,激起众怒又开架~何必何必~ VCL归Delphi RTL管,COM归Win32内核管,Delphi中的Interface最早先被设计出来,是与COM有关的。你不能指望borland一开始就设计好一个与COM完全无关的接口机制,还要让它会满足“用于COM”这样一个设计需求。 又,使用COM Interface,就不要让Delphi管理实例的生存周期。混用二者是灾难。但这是COM的机制决定的,根本不是、也不需要是要Delphi通过改变对象实例的结构来解决的问题。 |
| 关于语言的设计,我非常赞同Ruby的最小惊讶原则。当然了,这个原则在这里我要重新诠释一下,那就是,接口的使用不应该让不了解的人走到错误的路线上。是的,我们可以约束自己的调用规范。但是,请不要忘了,我们在讨论一个好的设计,而不是好的使用技巧。 |
| 支持aimingoo的观点! aimingoo说的其实在李维的书上说的很清楚了,楼主看来是只把注意力放到寻找"苛评"上了,忘了仔细看书了. 不过楼主"苛评"的精神是值的鼓励的. |
| 大概是楼主不太知道COM的实现机制吧。有些没办法,是由COM的机制限制的,不是一个FrameWork可以搞定的。 还有接口固然是VMT,可是不能和对象的VMT混用实现接口和对象的地址一致。这个道理楼主想一下就明白了。 还有你说的As操作符的反操作,可以非常容易的实现接口到对象的转变。只要给接口添加一个GetObject的方法,在方法的实现处直接写Result:=Self;就好了 |
| 优美的设计必然是平衡的。正如:E=mc^2一样,E转换成质量,质量也可转换成E。 现在,提供了从对象到接口的AS,却没有从接口的对象的AS! 另外,如果Delphi不提供AS,那么你是不是就在对象里增加一个方法叫GetInterface呢?这和增加GetObject是一个道理。不在于大家是不是知道怎么实现这个功能,而在于设计本身是不是考虑好这个问题。 |
| 对象指针和接口指针并不是同一个地址 如果我没误解你的话,我觉得这点并没影响, 但你讲这个设计是个缺点的话? 我会问你对象指针和接口指针是同一地址的情况可能吗? 对象和接口的指针地址都只是引用,是两个完全不同的变量,你不可能期望一个对象的引用和一个接口的引用使用同一个地址,就像你不能期望两个不同的变量使用同一地址一样. 但接口指针和对象指针实际指向同一对象是可以的. 这种情况是实际的对象存储在同一内存空间. 如果没有另一个不同的对象引用,我们的对象又从哪里创建出来? 接口的引用本身是不能创建对象的吧? 你讲的其它几个缺点我更加是不知所云了. (同样呀,"我等小辈水平太有限了") |
| 对象指针转为接口指针,这是可以理解的,但为什么要从接口指针转为对象指针呢?如果一定要这么做,很可能是设计上出了问题。 顺便提醒一点,如果你是准备从delphi的对象模型中来找结论的话,有一个问题是无法回避的:如果这个接口的实现类不是delphi写的呢? 就算这个实现类是delphi写的,你也得考虑一些问题,例如如果是用接口委托的方式实现的话,是不是会有问题。 写过一个比较变态的函数,如下: { 很变态的一个函数实现。 如果一个类实现了某些接口,现在已经拿到了指向接口的指针,但如果需要拿到对象指针的话, 需要用额外的比较麻烦的方法,这里提供一个函数可以方便拿到对象指针,该函数依赖于 delphi 的内部编译实现,如果这个类是由其他语言实现的话,不可以使用该函数。 因此通常情况下不建议使用该函数。 } function GetObjectFromIntf(Intf: IUnknown): TObject; begin Result:= TObject(Integer(intf) - ((not PByte(PInteger(PInteger(intf)^)^ + 4)^ + 1) and $000000ff)); end; |
| 看到这么多谦虚的高手,我发现我用了3年的delphi(虽然现在不用了)还基本等于白痴级。没有各位究根刨底的精神。 我发现其实大家都有个共同的,就是也同时在学习研究其它语言。。。 如果以在座各位的水平,能组织起来一起做点东西,也是件不错的事,提倡互相讨论进步,不提倡互相鄙视。 因为在delphi调用com事件响应时嫌搞得有点麻烦,而且还没实现。所以中断了学习。 顺便弱弱的文句,如果也变态的使用同一接口在单线程实现两个不同的对象(虽然一般情况下无此必要),如果接口指针与对象指针一致,那是不是说两个对象。。。,好象有点不太对劲。或者是接口VMT中也有了两处记录,com原理不懂,朋友们给解释下。 |
| 关于将INTERFACE转换为OBJECT,前提是: 在不使用COM的情况下,只是将INTERFACE做为一般的OO技术使用在设计中。 例如: 由FACTORY创建的对象通常得到的是一个INTERFACE,当这个对象被交还给FACTORY消毁时需要可能需要由INTERFACE转换为OBJECT。 当使用OBJECT POOL时,当传入对象时可能需要由INTERFACE转换为OBJECT。 等等,另外一点是,VCL的设计是基于OBJECT的,当在你的设计中对VCL中的类进行包装时(包装到INTERFACE中),尢其是涉及到VISUAL OBJECT时,往往会发生INTERFACE到OBJECT的转换。 INTERFACE转为OBJECT也不是什么奇怪的事,即然存在这么一项功能,那么就可以考虑:怎么用?用在哪?为什么用?不必急着贴标签,因为技术本来就是这么一回事,没有什么不可以的。 |
| 不要有所谓受制与MS 的心理阴影, 这带来的好处就是对COM非常好的支持, COM是一个伟大的划时代的设计,不要小看MS, Delphi从中也得到了灵感, 在MFC中,接口地址未必就是对象地址, 因为涉及到多个虚拟方法表之间的切换, 至于混用的问题,则是明知不能而为之, C++中的安全指针也有很多约束条件,你会故意的去违反这个 约束条件吗? 实际上,对比一下在c++ 和 delphi 中编写 COM对象和调用COm对象就会发现,TObject真是天才的设计。 |
| 各位高人,从TComponent与自定义的接口一并继承下来的类,怎样使用 接口转对象 操作? JCL里使用什么函数? |
| 参见:《JCL中由接口获得对象的方法》http://blog.csdn.net/xiammy/archive/2007/05/15/1610610.aspx |


fellow99 发表于2007-02-02 10:17:47 IP: 202.116.22.*