上一篇末留下的一个疑问这一回来作个解答吧。大家看了下面的图就清楚了:
结论就是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引用计数的目的.虽说是简单,还是给段代码吧
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对象的类型转换(其实是接口查询)的时候,还是与普通的拖管类的类型转换有一些细微的差别,但不是体现在代码上,而是体现在转换前后的两个类型的关系上:
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接口的三个方法,我们还是来看看具体的代码:
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接口,而不是一个其它什么类型,例如结构什么的.