C# 析构函数:Dispose“模式”和最佳实践

3
我知道在C#中析构函数和终结器的含义和用法有所不同。
然而,通常对于“我应该…”这样的问题的答案是“不要使用析构函数,而是使用MSDN中所示的释放模式”。Eric Lippert 写道强烈反对不必要地使用析构函数。
但是,这个“模式”建议编写一个析构函数,如下所示:~T() { Dispose(false); }。其声明的原因是作为“后备”,以防程序员忘记调用Dispose()。当然,这忽略了终结器在操作上是不确定的,并且可能根本不会运行。
因此:
  1. 如果我使用dispose模式,我是否也应该提供析构函数?顺便说一下,我只处理托管资源(例如Entity Framework的DataContext)。

  2. 如果我提供一个析构函数:如果我的类是从IDisposable派生的,而IDisposable可能已经提供了一个析构函数,那么我是否也应该提供一个析构函数?我认为在这种情况下永远不会编写析构函数,但是文档说它将自动调用基类的析构函数。


1
SO上已经有数十个类似的问题了。 - H H
4
你是否一开始就拥有一个“未受管理”的资源? - H H
没有关于其中任何一个的问题。我在问为什么这些答案中存在“进退维谷”的情况。 - Peter Marks
PS:我觉得MSDN的“模式”似乎是一种应对尽可能多边缘情况的万能解决方案。但它过于复杂,有时文档也令人困惑。 - Peter Marks
2
@PeterMarks 问题在于MSDN文档 - 它只记录了在使用非托管资源时的正确实现。它提到在其他情况下应该使用IDisposable,但是“参考实现”在这些情况下并不适用。 - Reed Copsey
@ReedCopsey 而且,由于它一开始就没有警告不要混合使用托管和非托管资源在同一个类中,所以这种反模式甚至比鼓励这种做法更糟糕。 - Jon Hanna
5个回答

18

我不会真的回答你的两个问题,但是我会提供一些意见:

所述原因是它是一个“后备”,用于在程序员忘记调用Dispose()的情况下调用。

如果要求方法的调用方传递非空字符串,那么如果他们传递null,您完全有权抛出异常,对吧?调用方违反了约定;这是异常行为,所以您会抛出异常。您不会认为,哦,调用方“忘记”传递有效参数,我认为我将接受错误的输入并继续前进。这样做实际上是改变了方法的合同,从“null不可接受并将产生异常”更改为“null是可以接受的,并被视为空字符串”,例如。

如果用户需要在完成后调用Dispose,而他们没有这样做,那么这与调用方法时调用方未能履行契约没有什么不同。调用方未能满足要求,因此应该崩溃他们的程序。如果析构函数遇到未释放的对象,则抛出一个信息丰富的异常。正如调用者很快学会了向方法传递错误参数的伤害一样,他们也会学会未能处理您的对象的伤害。

要么显式释放对象是必要的,要么不是。如果是必要的,则确保用户这样做。否则就是隐藏他们的错误


5
虽然我完全同意这个观点,但我建议有条件地将其构建为仅在DEBUG构建中包含析构函数也是一个好选择...如果纯粹是为了帮助正确使用,并没有真正的理由在发布构建中包含析构函数,因为它会对垃圾回收性能产生影响。 - Reed Copsey
2
@Eric:“崩溃他们的程序”……一如既往地富有表现力!:-)也许我会在异常中包含消息“你违反了我的合同!” - Peter Marks
1
@PeterMarks:说得好。Reed在上面提出了一个很好的观点;如果析构函数的目的仅是捕获用户的错误,那么将其作为调试代码可能是正确的做法。这就是我倾向于做的事情。对于没有非托管资源的类,我通常不会编写析构函数。 - Eric Lippert
20
你可以将异常称为"TasteJusticeException",并使错误信息为"你违反了我的合约,现在你要品尝正义!",这将很棒。 - Eric Lippert
2
如果您想避免与终结器相关的关闭时奇怪崩溃,那么还有一个要点,您可能还想在Finalizer中包装throw,并使用if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload())进行判断。 - Kevin Pilch
显示剩余3条评论

12
如果我使用dispose模式,我是否也应该提供析构函数?顺便说一下,我只会处理托管资源(例如Entity Framework DataContext)。
在这种情况下,不需要。原因是,在GC捕获您的类时,所有这些对象也将由GC处理。在这种情况下,没有理由增加析构函数的开销。
这是IDisposable的复杂性的一部分 - 根据用法,实际上应该有更多的标准实现。在这种情况下,您封装了一个实现IDisposable的资源。因此,允许用户(间接地)处理这些资源很重要,但是您不需要处理析构函数,因为没有直接“拥有”未托管的资源。如果您想了解更多详细信息,请参阅我关于IDisposable系列的第3部分
如果我提供析构函数:如果我的类从实现IDisposable的类派生而来,而该类可能已经提供了析构函数,那么我是否也应该提供析构函数?我认为在这种情况下永远不要编写析构函数,然而文档说它将自动调用基类的析构函数。
在这种情况下,基类应公开形式为protected virtual void Dispose(bool disposing)的受保护方法。您应该在那里放置资源清理逻辑,因为基类的析构函数会为您处理对此方法的调用。有关详细信息,请参见我关于IDisposable系列的第2部分

1
我认为你上面的评论中提到的“MSDN文档...仅在处理非托管资源时记录正确实现”的问题是真正的问题。那种模式感觉很安全,因为它经过了MS的审核,但对于托管资源的使用情况来说并不完全有意义。 - Peter Marks

2
如果你正在编写一个类,你不能强制所有使用该类的人都遵循预期的IDisposable模式。这就是为什么你需要析构函数的后备方案。
即使“每个人”只有“你自己”,你也是人,有时会犯错误。

返回已翻译的文本:True。但请记住,甚至不能保证其运行。 - Peter Marks
2
只有在非常罕见和特殊的类中才不要写成“a class”。 - H H
在这种情况下,这是不正确的。由于此类没有未托管的资源“所有权”,因此没有理由添加终结器的开销。如果需要,实体框架的上下文类应该自行管理它。 - Reed Copsey
@Reed:是的,这也是我的想法。但是为什么那个“模式”会得到如此多的关注呢?应该尽可能避免使用Finalisers(嘿,我都不记得上一次因为真正需要而编写Finalisers是什么时候了)。 - Peter Marks
@PeterMarks 模式是绝对必要的,因为它是处理资源清理的首选方法。然而,该模式确实比单个示例更复杂,因为适当的实现取决于您为什么实现 IDisposable,因为有4种明显不同的情况可以和应该使用它。请参见:http://reedcopsey.com/series/idisposable/ - Reed Copsey

2

在这里,已经有很多优秀的答案,很难再添加新的内容了。

我想给出一个MSDN上所倡导的dispose模式的替代方案。我从来没有真正喜欢过那个Dispose(bool)方法,因此我认为这种模式更好如果你绝对需要一个析构函数

public class BetterDisposableClass : IDisposable {

  public void Dispose() {
    CleanUpManagedResources();
    CleanUpNativeResources();
    GC.SuppressFinalize(this);
  }

  protected virtual void CleanUpManagedResources() { 
    // ...
  }
  protected virtual void CleanUpNativeResources() {
    // ...
  }

  ~BetterDisposableClass() {
    CleanUpNativeResources();
  }

}

但是,既然您已经发现您真的不需要一个,那么您的模式就简单得多

public class ManagedDisposable : IDisposable {

  // ...

  public virtual void Dispose() {
    _otherDisposable.Dispose();
  }

  IDisposable _otherDisposable;

}

+1 显式分离托管/非托管是整洁和合乎逻辑的。但你的方法缺少一个“TasteJusticeException”来强制执行代码契约(请参见Eric Lippert上面的答案,了解如何实现)。 - Peter Marks
1
你提供的文章很有趣且有用。 - Peter Marks
@PeterMarks:谢谢!我的方法缺少那个异常处理,因为我没有 Lippert 的聪明才智!不过你可以加上它! - Jordão

-1
 Or if you know that your object that you are trying to dispose of Implements IDisposable
why not do something like this

StreamWriter streamWrt = null
try
{
streamWrt = new StreamWrite();
... do some code here
}
catch (Exception ex)
{
 Console.WriteLine(ex.Message)
}
Finally
{
  if (streamWrt != null)
  {
    ((IDisposable)streamWrt).Dispose();
  }
}

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