程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#中COM操作(二) 接口查詢

C#中COM操作(二) 接口查詢

編輯:關於C#

上一篇末留下的一個疑問這一回來作個解答吧。大家看了下面的圖就清楚了:

結論就是t1,t2,t3是三個不同的引用,也就是說在.NET裡面代表了三種不同的類型,但是三種類型的GUID卻是一樣的,因為在COM裡GUID代表了一個COM類,只要GUID是一樣的那麼就表示是一個COM類,因此僅從COM類這一角度出發的話,這三種類型就是同一個COM類型。

第1種方式創建的COM對象的.NET包裝的類型一般來說就是COM導入的.NET包裝程序集裡面對應聲明的類型.

第2種方式創建的COM對象的.NET包裝的類型永遠都是__ComObject.

第3種方式創建的COM對象的.NET包裝或者是指針經過Marshal類的方法轉成的.NET的包裝,這兩種方式對應的類型__ComObject.

第4種從本質上來講是第1種方式的變種,只是更為靈活,使用范圍更加廣范了,因此對應的類型也應該是聲明的時候的.NET中的類型

上一文裡面留的第二個問題的結果就是原來是什麼類型,經過一次Marshal類的方法與IntPtr互轉換後的結果還是什麼類型,應該是CLR內部記錄了指針和.NET類型之前的對應關系,不會每次由IntPtr轉到object的時候都用一個不同的包裝(感覺有點像WinForm裡面從Handle找Control一樣).

上一篇我們講到了C#中創建COM對象的幾種方式。不知大家也注意到了,最後一種方式中JetEngineClass類並沒有提供方法供我們調用,要使用它的話必須先把這個引用轉成接口引用才能直接使用裡面的方法,實現早期函數綁定。雖然我們在聲明JetEngineClass類的時候並沒指定該類實現了IJetEngine接口,但是後面在使用的時候卻直接把engine用as操作轉成了IJetEngine接口,而且居然轉成功了。而且大家也可以用is操作符測試一下,engine is IJetEngine反回的結果也為true。這就是本篇要講的---C#中COM對象接口的查詢。

與COM創建的方法一樣,C#中COM接口查詢的方法也有好幾種:

第1種 Marshal.QueryInterface方法

這個方法本身就是Framework提供的正統的用來查詢COM對象的方法,這種方式MSDN上已經有詳細的說明了,我也不再多說.唯一注意的是這裡只能傳COM對象的指針IntPtr,而且這個方法成功返回後,所引用的COM對象的計數會自增1.以後當返回的查詢到的接口的指針不再使用了的時候,需要手動調用Marshal.Release,達到平衡COM引用計數的目的.雖說是簡單,還是給段代碼吧

1 IntPtr pJetClass = GetJetEngine(); // return JetEngineClass Ptr
2
3 IntPtr pJet;
4 Guid g = typeof(IJetEngine).Guid;
5 int hr = Marshal.QueryInterface(pJetClass, ref g, out pJet);
6 if(hr <0)
7   Marshal.ThrowExceptionFromHR(hr);
8

其實在使用IntPtr引用COM對象的時候,就像是在C++裡面直接使用COM指針一樣,理論上來說這個指針每復制一次,都需要我們手動的調用一次AddRef方法,增加COM對象的引用計數,每當我們把指針設置為無效或不再使用這個指針的時候,同樣需要手動的把這個指針用Release方法減少引用計數,當引用計數變為0的時候就釋放COM對象.這還是沒有擺脫C++裡面使用原始的COM指針的時候容易忘記平衡引用計數的問題.這裡我故意使用了"原始的COM指針"這外概念,主要是區別於在C++裡面我們常使用COM指針的另外一種方式COMPtr<T>泛型類,有了這個泛型類C++裡面的COM對象的引用計數就能夠正常及時的增加和減少了,使得開發人員不用花心思在COM引用計數的維護上.但是就算是這樣,要想查詢一個接口還是擺脫不了那個QueryInterface方法.

C#作為一種繼承了C++大部分優點的一種語言,當然也提供了類似的方式讓我們遠離引用計數的陷阱,而且還提供了更加優雅的方式供我們使用.

這就是我們要講的第2種COM接口查詢的方式

第2種 與C#語言一致的類型轉換方式

大家知道在C#裡面我們要想把一種類型的引用轉成另外一種類型有兩種方式,第一種類似於(IJetEngine)engine這樣;第二種類似於engine as IJetEngine這樣.這兩種方式有的時候產生的效果是一樣的,但是嚴格說來還是有很多差別的,這個在學C#的時候大家都遇到了,這裡我也不在多說,只是提幾個下面會用到的相同點和區別.

對於都是引用類型的轉換,大家都不產生新的對象,如果轉換成功的話都是返回指向給定對象的新的類型的引用.第一種強制類型轉換(暫且稱作這樣吧),在遇到轉換不成功的時候會拋出異常,但是大多數時候我們都不希望拋出異常,而是希望當轉換不成功的時候,返回null引用就可以了,而這正是第二種方式'as'方式所能夠達到的.

這兩種類型轉換方式同樣可以作用在COM對象的C#包裝的引用上,而產生的效果與前面用QueryInterface產生的效果是一樣的,都是返回一個給定的接口,只不過這裡以具體的接口聲明的引用代替了之前的接口指針.而且這種轉換方式與一個普通的C#托管類轉換到實現的接口的方式簡直是一模一樣.代碼風格的一致性也得到了更好的體現.

需要注意的是我們用這種方式用COM對象的類型轉換(其實是接口查詢)的時候,還是與普通的拖管類的類型轉換有一些細微的差別,但不是體現在代碼上,而是體現在轉換前後的兩個類型的關系上:

1 public interface IDemo
2 {
3 }
4 public class Demo : IDemo
5 {
6 }
7
8 public class Demo1
9 {
10 }
11
12 object o1 = new Demo();
13 object o2 = new Demo();
14 IDemo d1 = o1 as IDemo; // d1獲得了一個IDemo的引用
15 IDemo d2 = o2 as IDemo; // d2 值為 null
16
17 IJetEngine e = new JetEngineClass() as IJetEngine; // e獲得了一個IJetEngine的引用

從這裡我們可以看到普通托管類如果聲明的時候沒有實現某個接口,那麼在類型轉換的時候,一定不會轉成功,但是一旦某個托管類聲明成了COM類的包裝類以後,不管在聲明的時候有沒有實現相應的接口,只要所指代的COM類用QueryInterface能夠找到這個接口,甚至是一個聚合的接口,那麼這裡的轉換一定成功.在這裡類型轉換的功能就好像就成了QueryInterface的功能了. 同樣的C#裡面與as操作符是孿生兄弟的"is"操作符在這裡也不在是面向對象裡面的"is a...", "has a ..."的定義,變成了QueryInterface能不能成功的標志了.

第3種 聲明的接口從IUnknown接口派生,或包含IUnknown接口的三個方法,我們還是來看看具體的代碼:

1
2
3[ComImport, Guid("00000000-0000-0000-C000-000000000046")]
4public interface IUnknown
5{
6  void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
7  int AddRef();
8  int Release();
9}
10
11[ComImport, CoClass(typeof(JetEngineClass)), Guid("9F63D980-FF25-11D1-BB6F-00C04FAE22DA")]
12public interface IJetEngine1 : IUnknown
13{
14  void CompactDatabase(
15    [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
16    [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
17    );
18  void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
19}
20
21[ComImport, CoClass(typeof(JetEngineClass)), Guid("9F63D980-FF25-11D1-BB6F-00C04FAE22DA")]
22public interface IJetEngine2
23{
24  void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
25  int AddRef();
26  int Release();
27  
28  void CompactDatabase(
29    [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
30    [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
31    );
32  void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
33}
34
35IJetEngine1 iJetEngine = GetJetEngine() as IJetEngine1;
36IntPtr p1;
37iJetEngine.QueryInterface(typeof(IUnknown).Guid, out p1);
38
39IJetEngine2 iJetEngine = GetJetEngine() as IJetEngine2;
40IntPtr p2;
41iJetEngine.QueryInterface(typeof(IUnknown).Guid, out p2);

上面兩種方式都是正確的,需要注意的是如果把IUnknown的方法放到IJetEngine2接口內部聲明的話,必須放到函數聲明的最開始位置,想想虛函數編譯後函數指針的順序就明白了.不過這種方式有個不太好的地方就是搞了老半天好不容易才得到的一個對COM對象的包裝類,經過這麼一查詢接口,又回到了指針形態,很是不爽.

這裡說了幾種COM接口查詢的方式,無非就是COM對象的.NET包裝類的引用或者IntPtr指針轉來轉去的,這兩種COM對象的引用到底哪種更好點呢.我的建議是能用包裝類引用的盡量用包裝類引用吧,實在不濟的時候沒有聲明包裝類也可以用object作為引用類型.

我是不太喜歡直接操作COM對象的IntPtr指針的(非它類型的IntPtr指針除外,例如一個指向內存數據塊的指針),除非是實在沒有辦法的時候.原因嘛,就是因為COM引用計數器的問題.前面我們也提到過了,使用COM包裝類的引用的時候,不管在接口之間怎麼轉換,都不會產生新的對象;還有一點就是COM對象的引用計數只會在生成包裝類的實例的時候才會增加1;另外COM包裝類也是一個托管類,只不過是一個比較特殊的托管類而以,所以它的實例的生命周期還是遵循了一般托管類的生命周期的定義----當該對象沒有被任何一個變量所引用的時候,這個對象就需要被垃圾回收了.結合以上幾條,一個COM對象只被包裝類的實例引用時,在整個包裝類的生命周期內,COM的引用計數都只是1,直到包裝類被垃圾回收了,這個時候CLR會自動減少這個包裝類所指向的COM對象的引用計數,當計數器為0時COM對象也就被銷毀了.這個比C++裡面的ComPtr還要妙,ComPtr在每賦值一次的時候還要對引用計數加1呢.

回過頭來我們再看看使用IntPtr的情況,正如前面所說的,理論上來講每賦值一次IntPtr都需要對COM計數加1,每當一個有效的IntPtr不再使用了又要對其所引用的COM對象的計數器減1,對於現在C#程序員來說,很多甚至對內存的動態分配和釋放都沒有概念,更是會經常還要忘了COM計數器的這些操作,編程的樂趣就這樣被消磨得沒有了,何其痛苦呀.

另外就是在使用自己定義COM包裝類和接口的時候,經常會遇到一個接口的方法裡面用到了另外的接口,如果一層一層展開下去會需要聲明一大堆的接口定義,而我們其實中是需要其中的一個很少的功能,這樣太得不嘗失了.最簡單的方法就從我們的需要出發,保留我們需要調用的方法的接口的聲明,其它不相干的接口的參數用object類型或IntPtr定義,在用object作為參數類型的時候需要在參數上加上MarshalAs(UnmanagedType.Interface)特性,以表明這是一個COM接口,而不是一個其它什麼類型,例如結構什麼的.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved