介面的使用注意問題

hrdzkj發表於2013-06-12

From :

http://hi.baidu.com/pcplayer99/item/408ec138078d790bceb9fe13

 

 

2011-10-23 13:11

Delphi 介面使用中,物件生命週期管理,如何釋放需要注意的問題。pcplayer 原創!

網上有篇文章《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 錯誤!

 

相關文章