事由是这样的,C#使用了以前的一些COM组件,该COM组件使用多年,Bug相当少见,但是这回经常出现偶见问题。
COM内部有容器类,组件类,容器类释放后,内部组件变为无效。这里出现的故障就是经常出现组件还在,但是容器已被释放的情况。代码如下:
void UseCOM()
{
IContainer container = GetCOMObject(ContainerGUID);
IComponent component = container.GetComponent(index);
component.DoSomthing(); //此处偶见异常
}
经过调试COM代码,明确了是容器组件被释放引起的,但是一直搞不清内中缘由。这段代码在Delphi中使用了好久了。翻译过来就出问题。验证了好久,只能想到一种原因,那就是Delphi中对IContainer接口的引用计数是在函数最后一行才减一的,如果C#中出问题,那么一定是GC提前释放了container引起的。毕竟虽然容器和组件两个对象有内在的关系,可是从GC的角度,两者完全不相关,没有引用关系。只不过GC不一定何时释放容器而已。
这个看法很容易验证,如果将代码稍微修改一下:
void UseCOM()
{
IContainer container = GetCOMObject(ContainerGUID);
IComponent component = container.GetComponent(index);
component.DoSomthing(); //此处偶见异常
Marshal.FinalReleaseComObject(container);
}
那代码就完全没问题了,因为这相当于和Delphi的接口管理范围是一致的了。
之前最佳实践推荐,“对于成员变量,在用完之后要置为Null,而对于局部变量完全没必要,因为GC完全知道这一点”。这更支持了这一观点。最终在<.net 高级调试>中找到了佐证。在Release状态下,一旦对象不再被使用,GC即可回收,而不受变量有效范围的影响。而在Debug模式下,局部变量会一直持续到作用域结束才允许GC回收。
最终,去掉了Marshal一行代码的Debug代码证明了这个理论的正确性。在<MSIL权威指南>一书也有类似概念。感兴趣可以自行查阅。