學習COM編程技術也快有半個月了,這期間看了不少資料和別人的程序源碼,也嘗試了用delphi、C++、C#編寫COM程序,我的感受Delphi是最好上手的。C++的模版生成的代碼太過複雜繁瑣,大量使用編譯宏替代函數代碼,讓初學者知其然而不知其因此然;C#封裝過分,COM編程註定是要與操做系統頻繁打交道的,須要調用大量API函數和使用大量系統預約義的常量與類型(結構體),這些在C#中都需手工聲明,不夠簡便;Delphi就簡單多了,經過模版建立的工程代碼關係結構很是清晰,並且其能很是容易使用API函數和系統預約義的常量和類型(只需引用先關單元便可),但在使用過程當中也發現了一些缺點。【注1】c++
(1)有些類型(結構體)的成員類型與C++中的不是等效對應關係,如SHFileOperation函數的參數類型是SHFILEOPSTRUCT結構體,delphi中它的兩個路徑成員被定義成PWideChar型,與C++的LPCTSTR不一致,PWideChar是以空字符(\0)結尾的,導致這兩個成員不能包含多個文件路徑。【注2】編程
(2)有些接口的函數參數定義不一致,如IContextMenu.InvokeCommand函數參數在Delphi中是CMINVOKECOMMANDINFO類型,在c++中是LPCMINVOKECOMMANDINFO型 ,導致該接口函數不能使用擴展的CMINVOKECOMMANDINFOEX型參數。【注3】ide
Delphi操做COM的另外一便處在於他的接口的引用計數管理,這爲咱們寫程序解決了一大麻煩:不用管接口的AddRef和Release了,直接把接口當「接口指針變量」(【注4】)使用,編譯器會執行一些特殊的代碼自動維護接口的引用計數。固然,這也會帶來另外一個問題,接口至關於「變量」同樣使用,這就涉及到「變量」的生命週期問題,當把這樣一個局部「變量」經過強制類型轉換(【注5】)給一個全局變量時,待以後轉換回來時將引起錯誤。由於局部「變量」生命已結束,要被清理,其所表明的接口被減小引用計數釋放了,若是人爲讓「變量」AddRef一次,就能消除這個錯誤。函數
關於Delphi的接口引用計數管理,在網上看到的一篇介紹的文章,查好久了它的出處,目前已知最先是SaveTime於2004年2月3日發表於大富翁論壇。【注6】學習
下面將它整理了一下,以便加深對delphi對接口引用計數的理解。spa
接口是生存期自管理對象,即便是局部接口指針變量,也老是被初始化爲 nil。接口被初始化爲nil是很重要的,從下文中Delphi生成維護接口引用計數的代碼時能夠看到這一點。 操作系統
var MyObject: TMyObject; MyIntf, MyIntf2: IInterface; begin MyObject := TMyObject.Create; // 建立 TMyObject 對象 MyIntf := MyObject; // 將接口指向 MyObject 對象 MyIntf2 := MyIntf; // 接口指針的賦值 end;
當接口與一個對象鏈接時,編譯器會執行一些特殊的代碼維護接口對象的引用計數。例如以上代碼,當執行到MyIntf :=MyObject 語句時,編譯器的實現是: .net
1. 若是 MyObject <> nil,則設置一臨時接口指針 P 指向 MyObject 對象內存空間中的「接口跳轉表」指針(後面會分析「接口跳轉表」),不然 P := nil; 指針
2. 執行 System.pas 中的 _IntfCopy(MyIntf, P) 操做,進行引用計數管理。code
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,因爲大富翁論壇好像已關閉了,因此真正出處已無從考證。