.NET 常见问答:完成器(Finalizer)、程序集名、方法信息等等
.NET 常见问答...
——完成器(Finalizer)、程序集名、方法信息等等
原著:Stephen Toub
翻译:Abbey
原文出处:.NET Matters:Finalizers, Assembly Names, MethodInfo, and More
源代码下载:NETMatters0405.exe (139 KB)

非托管资源时才需要实现它?我是否一定要在我的完成器中实现 IDisposable 接口?反之又是如何的呢?

IDisposable 接口的实现表明你的类控制了需要被释放的资源,并且允许你的类用户决定是否要释放它们。因此,任何一个类实现了完成器就一定实现了 IDisposable 接口(如果垃圾回收器能自动释放资源,那么也应该允许开发者显式地调用某个方法来完成同样的工作)。但这并不是绝对的:并不是所有实现了IDisposable接口的类都要实现一个完成器。
设想一下,我的托管类有一个FileStream类型的私有成员。FileStream 控制了一个非托管的资源并且实现了 IDisposable 接口和完成器。当对该实例不再有引用时,FileStream就变成无法访问的与可终结的了。对我的类而言,它没有理由在注册对象队列里等待终结,因为内嵌的FileStream的实例已经被注册了。另一方面,考虑到我的类应该为用户提供方法以立即释放它所控制的资源,无论这种方法是直接地还是间接的,因此我的类应当实现IDisposable接口。我的Dispose()实现很简单,它只是简单地调用了FileStream的Dispose()方法。切记:尽管如此,你也需要特别小心地释放共享资源(比如正被别的实例使用的资源)。如果你编写一个类从外部释放资源而使该资源可用,请确保你的文档在这个主题上是清晰明确的,这样其他人就知道是否正在移交他们给你的那些资源的控制权了。
如果你的类不需要实现完成器,在你的Dispose()方法中应当调用GC.SuppressFinalize()方法,以确保系统不会去调用你的实例的完成器。这样你的实例同时也会从待终结对象集合中被删除,从而减轻了垃圾回收器在回收过程中的负担。贯穿于Microsoft .NET Framework中常见的实现模式是给Dispose()方法添加一个Boolean(逻辑)类型的参数。这个Boolean类型的参数指示这个类是否因IDisposable.Dispose()方法被调用或者完成器在运行而正在被释放(完成器与IDisposable.Dispose()都是委托到该方法上的)。如果确定它要被释放,GC.SuppressFinalize()就要被调用。如果是通过完成器被释放,就要避免再使用你的类中实现完成器的
托管成员,因为它们可能已经被终结了。
Figure 1 提供了一些指导性的说明,帮助你在合适的时候在你的类中实现这些结构。


Figure 2 中的ManagementObjectSearcher类来查询Win32_OperatingSystem.TotalVisibleMemorySize的值。因为 ManagementObjectCollection (由ManagementObjectSearcher返回)没有公开访问集合内部元素的方法,因此我使用了一个foreach循环来枚举出其中的每一个成员。而且因为我只关心其中的一个值,所以我在第一次枚举完成后就停止了循环。
注意TotalVisibleMemorySize返回的值可能并不是当前的物理内存总量,而是向操作系统报告可利用的内存量。你可以从Win32_OperatingSystem(Win32_OperatingSystem)这个WMI类中学会更多有用的东西。


static void Main(string [] args)
{
if (args.Length > 0)
{
try
{
AssemblyName a = AssemblyName.GetAssemblyName(args[0]);
Console.WriteLine(a.Fullname);
}
catch(Exception exc)
{
Console.WriteLine(exc.Message);
}
}
}
注意:同样的技巧也可以用到本地托管的DLL或者EXE上。你可以在这些文件上挨个地试一试。如果没有异常被抛出,并且返回了一个有效的名字,那么这个文件就是托管的。当然,这种方式在所有的本地文件都触发异常的情况下也存在一些性能上的缺陷。当然还有另一种方法,它不依赖于反射,也不需要装载Portable Executable (PE)。而是通过分析DLL或者EXE的PE头中某个标识位是否被置位,由此确定它是否是
托管的。Managed Extensions for C++ requently Asked Questions中有实现这种方法的C++代码,等价的C#代码请参见


((IDisposable)enumerator).Dispose();
如果枚举器没有实现IDisposable接口,那么编译器仍然不得不试着释放这个对象,因为不能确定枚举器的派生类型(从这个函数返回的)没有实现IDisposable接口。于是,编译器又会生成类似这样的一个finally块:
IDisposable disposable = enumerator as IDisposable;
if (disposable != null) disposable.Dispose();
因为大多数的可枚举类都有一个GetEnumerator()方法以返回IEnumerator接口(没有实现IDisposable接口),所以上述的代码只是编译器生成结果的一般情形。在少数情况下,当返回的类型没有实现IDisposable接口而且是密封的(指不能从其再派生新的类)时,编译器就不需要实现一个try/finally块了——因为没有什么需要释放的了。


Figure 4[/url]中的代码就是我的具体实现。ObjectWalker类实现了一个IEnumerator接口,这样就可以很容易地通过一个 foreach 循环就把对象层中的所有对象枚举出来了:
foreach(object obj in new ObjectWalker(hierarchy))
{
Console.WriteLine("{0}:t{1}", obj.GetType(), obj);
}
枚举时先将根对象压栈,每调用一次MoveNext()就出栈一次,再将Current数据属性返回的结果压栈。此外在我的实现里,我还会进一步地利用反射来列出对象内部可能的需要枚举的字段(译注:数据成员)值。如果对象内部有可枚举值,它会把这些枚举值取出来,并且使用ObjectIDGenerator来检测它们是否已经被处理过。反之,这些对象就会被放入处理用的栈以等待后续的操作取用。在检查字段前,这段代码会检查当前对象的类型是否符合终止条件。在这个实现里,当我遇到一些简单类型(比如int或者byte)、字符串string、一个枚举型或者一个指针时,就停止递归。我遇到指针时就停下来,这样可以防止我不慎进入非
托管的内存空间。
注意:在MoveNext()方法使用反射访问那些可能潜在的非公共成员时,这个类同时需要适当的反射权限ReflectionPermission才能保证MoveNext()成功完成。
译注:下面这个问答可以这么理解:需要使用 Control.Invoke() 调用一个方法 MyFunc(),于是用一个MethodInfo 对象 mi 描述了 MyFunc(),接着以 mi 为参数通过调用 Delegate.CreateDelegate() 返回 MyFunc() 的委托对象dl,最终将 dl 传递给 Control.Invoke() 实现对 MyFunc() 的间接调用。


MyDelegate d = (MyDelegate)Delegate.CreateDelegate(typeof(MyDelegate), theMethodInfo);
theControl.Invoke(d, new object[]{param1, param2, …});
如果这个方法并不是静态的,那么你可以使用Delegate.CreateDelegate()的另一个重载形式,使用将被调用方法的名字及其实例来产生之前需要的委托对象:
MyDelegate d = (MyDelegate)Delegate.CreateDelegate(typeof(MyDelegate),
targetObj,
theMethodInfo.Name);
theControl.Invoke(d, new object[]{param1, param2, …});
尽管如此,因为仍然需要一个该MethodInfo所对应的委托类型,并且你手里可能也还没有合适的委托类型,因此上述这种方法并不适用于所有的情形。更好的一个办法是建立一个适用于任何MethodInfo的委托类。要这样做,你首先要创建一个委托,它的声明形式对应了MethodInfo.Invoke()方法,第一个参数对应了对目标实例的引用(若是静态方法则是null),第二个参数则是调用所需的一个object类型数组。这个委托的声明可以这么写:
private delegate object MethodInvokeHandler(object target, object [] parameters);
之后你可以创建这个委托的一个实例,包装MethodInfo的Invoke()方法,然后将这个委托实例传递给Control的Invoke()方法,就象这样:
MethodInvokeHandler d = new MethodInvokeHandler(theMethodInfo.Invoke);
object [] parameters = new object[]{param1, param2, ...};
theControl.Invoke(d, new object[]{targetObj, parameters});
使用这种方法和之前定义的委托,你可以利用Control.Invoke()方法来调用任何一个MethodInfo对象修饰的方法。
另一种解决办法则是动态地生成一个委托类,这个类适用于形式匹配的方法(参见本月Paul DiLascia与Dino Esposito的专栏中关于代码生成的讨论。译注:即其中利用C#实现类似Spy++的事件查看功能的问答)。然后你就可以使用上文提到的Delegate.CreateDelegate()方法了。为什么你在已有上一种方案后还想动态地生成一个委托?因为先前的方案相对而言太慢了。与直接调用一个方法相比(从某种意义上讲,委托是一个很有效的面向对象的函数指针,尽管它的使用比直接的方法调用费时),该方案让你调用一个委托,通过迟绑定的操作来调用真正的方法。
因为动态生成代码会导致一定程度的运行性能下降,所以你一定要预先就生成这些动态代码,之后你就能在每一次的调用中获得速度上的优势了。如果你要进行一些调用,它需要花费一点启动时间。经我调用目标方法对传入的整数型参数执行一些简单的整数运算所完成的测试表明,使用动态生成委托的方法提高了50%的性能。当然,如果目标方法要真正用于实际应用,那么参数的类型还有商讨的余地。说了那么多关于方法调用的内容,其实与方法内部函数体的执行相比,方法调用的运行时间开销就无足轻重了,所以方法越简单越好。
在你的那些软件工程里,首先要保证代码是清晰的、可运行的。当其性能不能令人满意时,才反复不断地使用性能分析器确定影响性能的关键点并进行修改,以最终达到性能上的要求。


[url=http://vckbase.com/document/journal/vckbase32/figures/Random_CS.htm]random.cs。
System.Random并不是加密的,所以不能用于可能会威胁数据安全的应用中。相对的,在System.Security.Cryptography命名空间中的RNGCryptoServiceProvider类则是一个带加密功能的随机数产生器,可以应用于需要加密随机数的场合。

专栏 文章里,你回答了一个关于如何获取 CLR 安装目录路径的问题。你使用了P/Invoke() 来访问 mscoree.dll 暴露的非托管 GetCORSystemDirectory () 函数。为什么你会选择这种方法,难道没有别的方法了吗?

有什么疑问或者意见,请给我发Email:netqa@microsoft.com.
Stephen Toub 是 MSDN杂志的技术编辑。
译注:这是我接触编程类文章以来,感觉翻译难度最大的一篇,复杂的句子成份、大量的代词,而且其中的很多技术也是第一次接触,所以我对自己的这个翻译结果并不满意。在翻译过程中,我力求翻译的结果能清晰地表达作者的原意,所以并不一定是逐字逐句的翻译。我已然尽力了,希望大家能读懂它。
