在一个密封类上实现IDisposable

29

我不认为这个问题之前被问过。我有点困惑于在一个密封类上实现 IDisposable 的最佳方式,具体而言是在一个不从基类继承的密封类上实现(也就是我的杜撰术语“纯密封类”)。

也许你们中的一些人同意我所认为的实现 IDisposable 的指南非常令人困惑。话虽如此,我想知道我打算实现 IDisposable 的方式是否足够安全。

我正在编写一些使用 P/Invoke 代码,通过 Marshal.AllocHGlobal 分配 IntPtr ,自然而然,我希望能够清理掉我创建的非托管内存。所以我考虑这样做:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public sealed class MemBlock : IDisposable
{
     IntPtr ptr;
     int length;

     MemBlock(int size)
     {
           ptr = Marshal.AllocHGlobal(size);
           length = size;
     }

     public void Dispose()
     {
          if (ptr != IntPtr.Zero)
          {
               Marshal.FreeHGlobal(ptr);
               ptr = IntPtr.Zero;
               GC.SuppressFinalize(this);
          }
     }

     ~MemBlock()
     {
           Dispose();
     }    
}

我假设因为MemBlock是完全密封的,从未派生出另一个类,所以实现virtual protected Dispose(bool disposing)不是必要的。

此外,finalizer严格上是否必要?欢迎所有的想法。

4个回答

15

如果您忘记调用Dispose,那么终结器将作为一个必要的后备机制来最终释放非托管资源。

不,您不应该在sealed类中声明一个virtual方法。这根本无法编译通过。同时,在sealed类中声明新的protected成员也是不推荐的。


当然,在派生自基类的密封类的情况下,需要一个虚拟Dispose方法 - 对吗? - zebrabox
另外,Finalizer 意味着将对象加入到 Finalizer 队列中,并且需要实现实际的双重垃圾回收的开销。看起来使用非托管资源需要付出沉重的代价。难道没有避免性能损失的方法吗? - zebrabox
在这种情况下,您需要“覆盖”该方法。您不能将任何方法声明为“虚拟”的“密封”类。这是编译器的错误。 - Mehrdad Afshari
3
GC.SuppressFinalize防止终结和它的开销。 - Mehrdad Afshari
是的,我的错。我把override和virtual搞混了。当然,一个密封类根本不允许使用virtual! - zebrabox

11

一个小的补充:通常情况下,一个常见的模式是有一个Dispose(bool disposing)方法,这样你就知道你是否在Dispose(此时其他更多的资源是可用的)还是在finalizer(你不应该真的触碰任何其他相关的托管对象)。

例如:

 public void Dispose() { Dispose(true); }
 ~MemBlock() { Dispose(false); }
 void Dispose(bool disposing) { // would be protected virtual if not sealed 
     if(disposing) { // only run this logic when Dispose is called
         GC.SuppressFinalize(this);
         // and anything else that touches managed objects
     }
     if (ptr != IntPtr.Zero) {
          Marshal.FreeHGlobal(ptr);
          ptr = IntPtr.Zero;
     }
 }

是的,马克说得很好,但如果我知道我只处置了非托管资源,那么这是否是必要的呢? - zebrabox
还有一个非常愚蠢的问题,如果Dispose模式是为了确定性地释放非托管资源,那么为什么我要处理可处理的托管资源,当它们应该由GC清理? - zebrabox
如果你是决定性的,那么你会想要清理任何你正在封装的东西;特别是它们本身是IDisposable。你不会在终结器中这样做,因为它们可能已经被收集了(而且:这不再是你的工作)。你是对的;在这种情况下,除了SuppressFinalize(它并不是非常重要)之外,我们没有做任何托管的事情,所以不费心也没关系;这就是为什么我强调了一般情况。 - Marc Gravell
@zebrabox - Marc的观点总体上是正确的,但你有一个非常特殊的边缘情况。关于为什么,你的类可能拥有实现IDisposable的托管资源。然后你想在手动处理时处理这些资源,但不在终结器中处理。阅读Joe Duffy的文章(请参见我的答案中的链接)以获取更多信息。 - TrueWill
@TrueWill 和 @Marc Gravell。当然!(半滑稽地拍了拍头)现在有意义了。谢谢你们两个 :) - zebrabox

8

来自Joe Duffy的博客:

对于sealed类,不需要遵循此模式,这意味着您应该只使用简单方法实现您的Finalizer和Dispose(即在C#中的~T()(Finalize)和Dispose())。当选择后一种路线时,您的代码仍应遵循下面关于实现终结和处理逻辑的准则。

所以是的,你应该没问题。

正如Mehrdad所提到的,您确实需要finalizer。如果要避免它,可以看一下SafeHandle。我对P/Invoke没有足够的经验来建议正确的用法。


谢谢TrueWill!我已经看过SafeHandle了,根据Eric Lippert的说法,它被BCL团队视为“Whidbey”中引入的最重要的好处之一(很抱歉现在找不到链接)。不幸的是,它是一个抽象类,所以你必须为每种情况自己编写代码,这有点糟糕。 - zebrabox
1
@zebrabox:虽然在某些情况下可能需要自己编写,但文档中指出:"预先编写的一组从SafeHandle派生的类作为抽象派生提供,并且此组位于Microsoft.Win32.SafeHandles命名空间中。" - TrueWill
1
@TrueWill。没错,这只适用于像文件句柄、等待句柄、管道句柄和一堆加密相关的东西。但总比没有好! - zebrabox
1
@zebrabox:说得好。看起来创建一个子类并不难,而且每种句柄类型/场景只需要做一次。我在http://blogs.msdn.com/shawnfa/archive/2004/08/12/213808.aspx找到了一篇不错的文章——它是预发布版本,所以您可能需要根据MSDN文档验证信息。 - TrueWill
@TrueWill。感谢提供链接。我会进一步查看。 - zebrabox

1

在密封类中无法声明虚方法。同时,在密封类中声明受保护成员会导致编译器警告。因此,您已经正确实现了它。

在终结器内调用GC.SuppressFinalize(this)是没有必要的,但也不会有害。

当处理非托管资源时,拥有终结器是必不可少的,因为它们不会自动释放,您必须在终结器中进行释放,该终结器在对象被垃圾回收后自动调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接