问题: 在我的类中何时需要实现一个完成器?我是否一定要实现完成器,或者只是在我控制着 非托管资源时才需要实现它?我是否一定要在我的完成器中实现 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。正因它能防止你两次遍历同一个对象,所以你可以很容易地避免循环引用了。
查看本文来源