網上有篇文章《Delphi介面程式設計的兩大陷阱》,裡面提到介面的生存期管理的問題。但該文章裡面提到的兩個問題,其實都是對 Delphi 不理解導致的。
先說該篇文章中提到的第一個問題為什麼是該文章作者不理解 DELPHI 導致他認為那是不可理解的陷阱。然後俺再來重點解釋介面的生命週期管理。
一. 介面 - 物件。
假設有介面定義:
IMyTask = interface
procedure SayHello;
end;
然後有個類實現了該介面:
TMyClass = class(TComponent, IMyTask)
public
procedure SayHello;
end;
然後有個該類的物件例項:MyObj := TMyClass.Create(Application); 和一個介面變數定義 MyIntf: IMyTask;
這時候,按 DELPHI 的語法規則,當然可以這樣做:
MyIntf := MyObj as IMyTask;
也可以不用 AS 這樣寫:MyIntf := MyObj;
其實還有很多其它寫法。總之,這裡是從 MyObj 物件例項,取得它實現的介面的指標!而不是做型別轉換!
前面提到的那篇文章的作者卻以為這樣做是【型別轉換】,因此他就試圖通過型別轉換來做:MyObj := MyIntf...這樣做當然是不行的!因為他的理解錯誤,使得他認為那是個陷阱。
事實上,一個物件可以實現多個介面。比如 TComponent 肯定也實現了IInterfaceComponentReference 介面。因此,我們還可以:
AInft: IInterfaceComponentReference;
AIntf := MyObj as IInterfaceComponentReference;
注意到沒,一個物件可以擁有多個介面,因此,獲取的該物件的介面的指標,肯定不是指向該物件的。否則兩個介面的指標就打架了!所以,這裡不能直接做型別轉換,把指標轉為物件!
那麼,當我們有一個介面,如何獲得它所在的物件呢?TComponent 實現的 IInterfaceComponentReference 介面剛好就幫我們實現了這個:function GetComponent: TComponent;
因此,如果是繼承自 TComponent 的物件,它肯定有實現 IInterfaceComponentReference介面。因此,上面的 MyIntf: IMyTask 這個介面就可以通過以下方式獲得它的物件指標:
AObj : TComponent;
AObj := (MyIntf as IInterfaceComponentReference).GetComponent;
------------------------------------------------------------
二. 介面生存週期的管理:
在 Delphi 裡面使用介面,一個實現某個介面的類,必須實現 IInterface 介面的三個方法。
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
上述3個方法裡面,_Release 方法就負責釋放物件自己。
當然,我們自己寫一個類的時候,不用自己去實現這三個方法,只要我們的類從 TInterfacedObject 繼承。TInterfacedObject 類已經實現了上述三個方法。
繼承自 TInterfacedObject 的類,其物件被建立後,要使用該物件的方法、屬性,俺隆重建議你引用該物件的介面,而不是引用該物件的物件例項。因為 TInterfacedObject 類裡面已經寫好,當介面引用計數為 0 的時候,自動釋放該物件本身。也就是說,你的程式裡到處使用該物件的某個介面,只要你想釋放該物件,則只需要將對該介面的引用設定為 nil,也就是隻要沒有任何變數引用到該介面,該介面對應的物件例項會自動被釋放掉。這樣一來,你就不會有記憶體洩漏的問題了!
但是.......但是.......但是來了,需要注意的事情來了!
如果,你的類,是從 TComponent 類繼承下來的,而且實現了你自己寫的某個介面..........
TComponent 類的物件例項的介面引用為 0 的時候,並不會釋放物件例項自己!你必須自己去釋放物件本身,呼叫物件的 Free 方法。
這還是比較簡單的概念。麻煩的是,如果你的物件A,擁有另外一個物件B的介面引用,當物件A被釋放的時候,A內部的介面引用自然變成 nil,則會導致物件B內部的引用計數減一。問題是,如果在這之前,物件B已經被 FREE 了,這時候就會出現 AV 錯誤。
因此,這時候,一定要注意釋放順序!
當你給某個 TForm 的子類比如 TForm2 增加一個你自己定義的介面的時候,這樣的錯誤就容易出現了。
以下是俺對該使用場景做的總結,此總結經過俺自己寫程式碼測試確認過:
1. 如果該 Form 是屬於 Application 的,則程式退出時,該 FORM 比主FORM先被消滅;
2. 如果主 FORM 裡面引用了該 FORM 實現的介面,則主 FORM 被消滅時,
delphi 內部會自動將該介面引用置為 nil,導致 nil 了一個物件已經不存在的介面,
導致 AV 錯誤。
3. 但是,如果執行期,用按鈕釋放該 FORM,然後再用按鈕事件設定該介面為 nil,
也就是設定了一個物件不存在的介面為 nil,並不會導致 AV 錯誤;
4. 實現介面的 FORM,執行期如果只釋放其介面,其物件例項不會被釋放
(好像繼承自 TComponent 的類都是這樣的)
5. 再次測試,如果被建立的有介面的 Form 不屬於 Application 而是其 Owner 是建立它的主 Form,
則不需要在主FORM被銷燬之前,做任何釋放它的介面的動作,也不會出現 AV 錯誤。
再次重複一下上面的說法:
因此,程式退出時,要在主 FORM 裡面的 OnClose 裡面主動釋放介面。
這裡可以不用釋放 FForm,等程式真正關閉時由 Application 來自動釋放這個 Form。
也就是說,在這個 Form 釋放前,必須先釋放它被引用的介面。
這段測試也說明,主FORM的 OnClose 比其它 Form 的被釋放更早一步執行。
一個普通的 Delphi 程式在關閉程式的時候,大概的執行順序:
關閉主 FORM - 主 FORM.OnClose -- 其它 FORM 逐個被銷燬 -- 主 FORM 被銷燬。
總結:一定要注意釋放順序。而釋放順序要遵循的原理,則是 TComponent 的子類,釋放完介面,物件不會自動釋放,必須主動釋放物件;但物件被釋放以後再釋放引用它的介面(比如 MyIntf := nil; )則會因為釋放介面會使得執行物件內部引用計數減一,而該物件又已經不存在,去執行一個不存在的物件內的程式碼,導致 AV 錯誤!