Chinaunix首页 | 论坛 | 博客
  • 博客访问: 665311
  • 博文数量: 946
  • 博客积分: 52360
  • 博客等级: 大将
  • 技术积分: 13080
  • 用 户 组: 普通用户
  • 注册时间: 2008-08-07 17:31
文章分类

全部博文(946)

文章存档

2011年(1)

2008年(945)

我的朋友

分类: C/C++

2008-08-07 17:39:12

下载本文示例代码

原文出处:.NET Matters:Finalizers, Assembly Names, MethodInfo, and More

源代码下载:NETMatters0405.exe (139 KB)


在我的类中何时需要实现一个完成器?我是否一定要实现完成器,或者只是在我控制着 非托管资源时才需要实现它?我是否一定要在我的完成器中实现 IDisposable 接口?反之又是如何的呢?

完成器只是在你控制了需要被清除的资源的才需要实现。举个例子,FileStream 控制了一个本地的文件句柄并且实现了一个完成器,这样也就保证了 FileStream 被垃圾回收器回收时能释放这个句柄。不幸的是,完成器给垃圾回收带来了一定意义上的负担,因此应仅在必要时才使用完成器。
  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 提供了一些指导性的说明,帮助你在合适的时候在你的类中实现这些结构。

我希望把一些关系到应用程序性能的操作建立在计算机可用内存的基础上。如何才能最简单地从操作系统获取这些信息?

尽管我知道获取这类信息其他的一些方法,但当我发现WMI (Windows Management Instrumentation,Windows管理规范)时,我发现它才是完成这类工作最佳的方式。Win32_OperatingSystem类提供了关于操作系统事件的丰富信息,System.Management命名空间则提供了大量的类来访问WMI的数据。你可以使用 Figure 2 中的ManagementObjectSearcher类来查询Win32_OperatingSystem.TotalVisibleMemorySize的值。因为 ManagementObjectCollection (由ManagementObjectSearcher返回)没有公开访问集合内部元素的方法,因此我使用了一个foreach循环来枚举出其中的每一个成员。而且因为我只关心其中的一个值,所以我在第一次枚举完成后就停止了循环。
   注意TotalVisibleMemorySize返回的值可能并不是当前的物理内存总量,而是向操作系统报告可利用的内存量。你可以从Win32_OperatingSystem(Win32_OperatingSystem)这个WMI类中学会更多有用的东西。

我尝试着在未将程序集装载入我的AppDomain的情况下,获取该程序集的完全限定名。这可能做到吗?

绝对能!System.Reflection.AssemblyName类有一个static类型的方法GetAssemblyName(),这可以返回磁盘上一个程序集的名称AssemblyName。这个方法只是简单地打开这个程序集文件,而不会将它装载入AppDomain。下面的这段代码,就能在控制台上输出从命令行传入的路径参数对应程序集的完全限定名:
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#代码请参见 Figure 3。

在我的C#应用程序里有一大堆的foreach循环。当我检查编译器生成的MSIL 文件时(Microsoft intermediate language,Microsoft中间语言)时,我发现其中有的循环被嵌入了一个try/finally块,但我的源代码里并没有使用啊?它们为什么会在这里出现?

问得好!记住foreach循环是用来枚举那些集合型的数据的。它是通过获取一个枚举器后,使用枚举器的MoveNext操作来实现遍历,并在Current数据属性中返回集合中的当前项。这个枚举器本身则是通过调用该集合对象的GetEnumerator()方法获得的。因为枚举器可能实现了IDisposable接口,所以C#的编译器需要在枚举完成后将其释放。为此,编译器会将这个循环放入一个try块,然后试着在finally块中释放这个枚举器。如果利用GetEnumerator ()方法返回的枚举器实现了IDisposable接口,编译器就会生成一个类似下面这样的finally块:
((IDisposable)enumerator).Dispose();
如果枚举器没有实现IDisposable接口,那么编译器仍然不得不试着释放这个对象,因为不能确定枚举器的派生类型(从这个函数返回的)没有实现IDisposable接口。于是,编译器又会生成类似这样的一个finally块:
IDisposable disposable = enumerator as IDisposable;
if (disposable != null) disposable.Dispose();

因为大多数的可枚举类都有一个GetEnumerator()方法以返回IEnumerator接口(没有实现IDisposable接口),所以上述的代码只是编译器生成结果的一般情形。在少数情况下,当返回的类型没有实现IDisposable接口而且是密封的(指不能从其再派生新的类)时,编译器就不需要实现一个try/finally块了——因为没有什么需要释放的了。

我希望能枚举出一个对象层中的所有对象,但是我不知道如何才能避免很有可能产生的循环引用。.NET Framework对此提供了什么帮助没有?

遍历一个对象层中的所有对象并以某种形式将它们序列化,非常类似于完成一个BinaryFormatter那样的远程格式化程序。对此,.NET Framework也提供了一些很有用的类来完成这类工作。System.Runtime.Serialization命名空间中的ObjectIDGenerator类可以看作是一张专门化的表,用于跟踪对象并赋与每个对象一个唯一的标识符。你可以通过这个类查询对象的ID——它既可以向你提供继存对象的ID,也可以为一个未出现的对象生成一个新的ID。正因它能防止你两次遍历同一个对象,所以你可以很容易地避免循环引用了。此外,System.Collections命名空间里集合类型的类也能帮你很容易地跟踪需要被检查的对象。
  Figure 4 中的代码就是我的具体实现。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() 的间接调用。

在我的 GUI 应用程序中,我有一个 MethodInfo 对象描述了一个用以修改主窗体上控制的方法。这个 MethodInfo 对象可以被另一个系统使用反射获取,并且可用以描述很多不同形式的方法。当这段代码在一个工作线程(辅助线程)中执行时,我需要使用这个控制的Invoke()方法在这个线程中来执行上文中的方法,以获取该控制的底层Windows句柄。但这样做,需要一个我所要执行方法的委托对象。我如何才能将我的这个MethodInfo对象转换成一个委托对象传递给这个Invoke()方法?

如果你已经有了一个委托类,它的形式对应了MethodInfo对象所描述的方法,并且这个方法是静态的,那么静态的Delegate.CreateDelegate()方法才是你需要的。将这个要实例化委托的Type对象与包装静态方法的那个MethodInfo传递给Delegate.CreateDelegate(),它就会给你返回正确的委托实例。这个委托实例接着可以被传递给那个控制的Invoke()方法,就象下面段代码一样:
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%的性能。当然,如果目标方法要真正用于实际应用,那么参数的类型还有商讨的余地。说了那么多关于方法调用的内容,其实与方法内部函数体的执行相比,方法调用的运行时间开销就无足轻重了,所以方法越简单越好。
   在你的那些软件工程里,首先要保证代码是清晰的、可运行的。当其性能不能令人满意时,才反复不断地使用性能分析器确定影响性能的关键点并进行修改,以最终达到性能上的要求。

System.Random类使用了什么算法来产生随机数?这个算法是加密的吗?

在目前这个版本的.NET Framework中,System.Random类使用了Donald Knuth描述的一个简化算法(The Art of Computer Programming, Volume 2: Seminumerical Algorithms (Addison-Wesley, 1997))。你可以在SSCLI(Shared Source Common Language Infrastructure,可共享的源通用公共语言规范)中查看System.Random的源码,或者在线查看 random.cs。
  System.Random并不是加密的,所以不能用于可能会威胁数据安全的应用中。相对的,在System.Security.Cryptography命名空间中的RNGCryptoServiceProvider类则是一个带加密功能的随机数产生器,可以应用于需要加密随机数的场合。

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

据我所知,至少还有两种简单的方法获取这个目录路径。第一种方法最简单,调用System.Runtime.InteropServices.RuntimeEnvironment 类的 GetRuntimeDirectory() 方法即可。第二种方法则是先获取 mscorlib.dll 中某种类(比如对象类)的类型,然后使用该类型的 Type 对象来获取 mscorlib 模块的全限定名。在一台安装了 .NET Framework 1.1 的 Windows XP 机器上,mscorlib 的路径可能是这个样子的 %windir%\microsoft.net\framework\v1.1.4322\mscorlib.dll。System.IO.Directory.GetParent() 方法可用以获取其父文件夹的路径。尽管 RuntimeEnvironment.GetRuntimeDirectory() 是最方便的,但它并不是最快的。在我的机器上,调用它来获取该目录路径的时间开销近 5 倍于等价的 interop 方法调用。而使用 Type 对象的方法则更甚,近15倍于 interop 方式的方法调用。当然,这些函数并不需要在一个长时间运行的循环中被调用,所以它们的效率高低并不是什么大问题。我个人认为 interop 是一种很酷的技术,而且 interop 的速度也不赖,所以才想到展示上期提到的那种方法的。
有什么疑问或者意见,请给我发Email:netqa@microsoft.com.

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

阅读(108) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~