资源管理与垃圾回收器的复杂性

在软件开发中,资源管理是一个至关重要的话题。原始的想法非常简单:垃圾回收器(Garbage Collector)会为清理内存和资源,因此不需要手动管理内存和资源。然而,在实践中,情况要复杂得多。垃圾回收器可能不会在希望它运行的时候运行(因为它是“非确定性的”)。此外,静态字段会保持对象的生命周期,代码可以与本地代码交互,需要对对象和内存生命周期有更多的控制。这就证明了需要有终结器(finalizers)来在垃圾回收器运行时释放外部资源,同时也需要Dispose()方法来“尽快”释放任何资源,而不需要等待垃圾回收器运行。这就产生了一个雪球效应,改变了整个类层次结构以实现一个硬模式,因为通常希望控制资源何时被释放(Dispose()方法和IDisposable接口),同时也需要一个终结器,因为任何“非托管”数据都需要被释放,即使(或代码的用户)没有调用Dispose()。

原始解决方案是Dispose模式。这并不容易,因为它包括:有一个终结器(调用Dispose(false));有一个Dispose()(调用Dispose(true)和GC.SuppressFinalize(this));有一个重载的Dispose(bool disposing),它根据disposing决定实际要做什么。这个重载可以是虚拟的,也可以不是,增加了模式的复杂性。

Dispose模式的“雪球”效应是,需要决定一个对象是否需要可处置。这本身就是有问题的。Dispose模式与基类和框架的交互非常糟糕。如果一个框架对象可能需要“可预测的”销毁,这意味着需要一个Dispose()或类似的方法。但是,作为一个框架或基类,这也意味着子类的对象可能拥有非托管数据,这意味着“需要”终结器。现在,整个模式需要在任何可能处理非托管数据的基类上使用。

因此,不是有像下面这样简单的东西:

public abstract class MyBaseClass : IDisposable { public virtual void Dispose() { // 清理代码 } }

相反,需要有像下面这样的东西:

public abstract class MyBaseClass : IDisposable { ~MyBaseClass() { Dispose(false); } public void Dispose() { GC.SuppressFinalize(this); Dispose(true); } protected virtual void Dispose(bool disposing) { // 清理代码 } }

重要的是要注意,在第一段代码中,Dispose()是虚拟的。在第二段代码中,Dispose()不应该被虚拟,相反,Dispose(bool disposing)需要是虚拟的... 并且也不应该是公开的,因为它不应该被用户代码调用。

当最初看到带有“disposing”参数的Dispose()时,感到非常困惑。在一个已经命名为Dispose的方法中,“disposing”是什么?真的认为,如果Dispose模式被命名为Release模式,有一个isFromManualDispose参数,事情会少一些混乱。它仍然是一个有问题的模式,但它会更容易理解这个参数意味着什么。

为什么这个模式是“坏”的?“坏”这个词太强烈了,说实话,但想吸引读者的注意。它是“坏”的,因为它依赖于太多的方法和概念,它也很混乱。即使“它工作”当正确实现时,它:

  • 对新开发人员来说很难;
  • 意味着任何框架类都需要有一个Dispose(bool)来处理可能的“不安全”数据,即使整个框架不使用任何不安全代码;
  • 意味着任何继承这些类的人都需要知道如何处理那个“可恶的”disposing参数;
  • 意味着它破坏了单一责任原则。一个框架类(或任何基类)不应该处理所有这些“以防万一”一个子类需要它。

解决方案:SafeHandles。后来,微软注意到了这个糟糕的模式,并试图修复它。那就是得到SafeHandles的时候。拥有SafeHandles的整个想法是,而不是让自己的类处理“托管和非托管内存”,类应该只处理托管内存,或者,如果真的需要,处理一个“安全句柄”,这是一个将有终结器的对象,并将真正管理非托管数据的生命周期。

真的认为微软在一开始的文档中就做对了,但当试图找到他们的好例子时,只找到了最新的文档,它完全“坏了”。他们解释了为什么SafeHandles是好的,并帮助避免了糟糕的Disposable模式,但然后展示了一个使用SafeHandle并且也实现了Dispose模式的类,没有任何真正的好处!在阅读文档后,似乎现在在艰难的模式之上有了新的模式。但这是错误的。新模式的目的是取代旧的,艰难的模式。而不是添加到它。

对于那些好奇的人,指的是这个页面。在那个页面上,源代码中甚至有这个评论:

// No finalizer is needed. The finalizer on SafeHandle // will clean up the MySafeFileHandle instance, // if it hasn't already been disposed. // However, there may be a need for a subclass to // introduce a finalizer, so Dispose is properly implemented here. [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] protected virtual void Dispose(bool disposing)

关于子类引入终结器的评论是Dispose模式的另一个问题。有些人认为不应该添加终结器,直到需要它,但然后如果基类决定使用终结器,可能会有双重释放发生。应该避免所有的混乱;

“However”这个词在那个样本中拼写错误了。编辑们,请在这里的文章中不要修复它;他们说“Dispose在这里正确实现”,但实际上他们忘记了检查disposing参数。他们不应该在disposing为假时调用_handle.Dispose()。

“新”模式SafeHandles的创建是为了简化模式。当使用SafeHandles时,不应该担心整个Dispose模式。相反,只需要知道否实现Dispose()。只是简单的Dispose(),不是那个奇怪的Dispose(bool disposing)。

然后,如果处理Windows句柄,使用适当的安全句柄,如果一个对象从未被处置并且被垃圾收集,SafeHandles为做工作。实际上,这是一个新的和改进的模式的基础,即使没有SafeHandles来处理正在处理的非托管内存或数据。

当不使用SafeHandles时,新模式是什么?简单规则:

  • 公共类不应该有析构函数或Dispose(bool)。如果它们是可处置的,它们只实现IDisposable与标准Dispose();
  • 如果它们使用任何可能需要析构函数的数据,它们应该使用一个辅助类来持有该数据。这正是SafeHandles:辅助类持有数据并为处理析构函数(没有其他)。

在某种程度上,就是这样。如何实现其中一个辅助类?辅助类将需要有一个析构函数,并且可能需要一个Dispose()来允许早期释放资源。但这些辅助类可能是密封的,避免任何逻辑来处理托管+非托管数据。它们的存在的唯一目的是处理非托管数据的释放,所以没有必要检查那个。它们不应该做任何其他事情,因为那将是主类的工作。它们只是简单的助手。

重新分析问题Dispose模式:具有Dispose(bool)的类,一个调用Dispose(false)的终结器,一个调用Dispose(true)的重载Dispose(),以及那个bool disposing让许多开发人员想知道到底发生了什么,即使他们的类从未使用非托管数据。新模式:如果类需要确定性清理,只需简单实现IDisposable接口,如果类可以使用虚拟的,如果类使用任何非托管数据,一个辅助类(可以是所有非托管数据的相同,如SafeHandle)。就是这样。没有disposing参数,也没有因为这样的参数而产生的奇怪的实现。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485