Dispose/finalize模式:处理托管资源



假设我有一个名为Base的类,它有3个属性:

class Base : IDisposable
{
private string _String;
private Class1 classe1;
private int foo;
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
public virtual void Dispose(bool disposing)
{
if (disposing)
{
Console.WriteLine("Free Managed ressources");
//HOW TO FREE _String, class1 and foo ?!          
}
Console.WriteLine("Free unmanaged ressources");
}
~Base()
{
this.Dispose(false);
}
}

和一个名为Class1的类,具有两个属性:

class Class1
{
public int entier { get; set; }
public string Nom { get; set; }
}

我的问题是:如何在Dispose方法中释放Base的属性?(_String,classe1,foo)

我的问题是:如何在Dispose中释放Base的属性方法(_String,classe1,foo)

您不需要,这是垃圾收集器的工作。实现IDisposable是框架的一种方式,它允许您释放已分配的任何非托管资源,并释放实现IDisposable的托管对象本身(这些对象又包含其他非托管资源)。

一次性使用的托管对象中没有一个实现IDisposable,一旦不再有任何对象指向Base类,它们就会被收集。什么时候会发生?在任意时间,当GC看到第0代中不再有空间时,它需要收集。你不需要做什么。

实现IDisposable并不意味着"一旦我运行Dispose(),就会立即收集此对象",它只是意味着该框架让您有机会回收它可能不知道的任何资源(例如非托管资源)。如果实现了终结器,建议采用的方法是通过GC.SuppressFinalize抑制对它的调用,从而避免GC将对象从终结器队列移动到F-可达队列的麻烦,从而使其可以更早地进行收集。

这3个属性何时从堆中释放?垃圾收集器不会释放它们,因为我有GC。SuppressFinalize(这个)

您对GC的工作方式和SuppressFinalize的含义有一个基本的误解。GC将在一个不确定的时间运行,您基本上不应该关心这种情况何时发生。在你身后打扫卫生是他的责任。在实现终结器的对象上调用SuppressFinalize只不过是在对象标头中设置一个位,运行时在调用终结器时会检查该位,这将禁止终结器运行

在这种情况下,您根本不应该实现IDisposable,或者如果它在那里,因为人们认为它在未来很可能是必要的,那么它将有一个空的实现。你当然不应该有一个最终确定者;除非你真的需要一个百分之百确定的,否则永远不要有。

在某些情况下,您希望实现IDisposable,在其中一些情况下,还希望拥有一个析构函数(这是C#拥有finalizer的方式)。

一种是当对象完成时,你有一些非常重要的事情要做,通常是撤销你以前做过的事情,比如释放你获得的句柄,关闭你打开的连接,等等,但不是托管内存。(所有对象都使用托管内存,如果无法再次使用,并且其他对象需要更多的托管内存,则所有对象都会清理其托管内存,这就是"托管内存"中托管的含义)。

public class SomeClass : IDisposable
{
private IntPtr _someHandle;
public SomeClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
}
public void Dispose()
{
ReleaseHandle(_someHandle);
}
}

因此,现在,每当使用SomeClass的东西用它完成时,它都会调用它上的Dispose()(可能是通过using块隐式调用),所有这些都会很好地清理掉。

但如果没有发生呢?这就是为什么我们可能会有一个最终确定者:

public class SomeClass : IDisposable
{
private IntPtr _someHandle;
public SomeClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
}
public void Dispose()
{
ReleaseHandle(_someHandle);
_someHandle = null; // so we know not to release twice.
}
~SomeClass()
{
if(_someHandle != null)
ReleaseHandle(_someHandle);
}
}

因此,在这里,如果Dispose()没有被调用,我们仍然会得到清理,因为正常的垃圾收集过程:

  1. 意识到你需要更多的内存
  2. 查找不再使用的对象
  3. 回收那些对象的内存

添加了以下步骤:

  1. 意识到要回收内存的对象有一个Finalizer要运行
  2. 将该对象放入其他此类对象的队列中
  3. (在一个单独的线程上)运行对象的finalizer
  4. 根据上面的步骤4,该对象不再是一个"有一个Finalizer要运行"的对象,所以下次可以回收它

所有这些都有缺点:

  1. 我们不能保证什么时候会发生这种情况
  2. 在步骤3中,我们没有回收那么多内存,因为有这样一个对象
  3. 垃圾收集是一代一代的,很好地处理对象的代收集意味着要么很快死亡,要么活很长时间,在GC第一次尝试收集对象后死亡几乎是最不理想的时间

我们可以通过调用Dispose()来绕过前两个,而不是让最终确定发生,这取决于类的用户,而不是类本身。我们通过让一个知道不需要最终确定的对象将自己标记为不需要:来绕过第三个问题

public class SomeClass : IDisposable
{
private IntPtr _someHandle;
public SomeClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
}
public void Dispose()
{
ReleaseHandle(_someHandle);
GC.SuppressFinalize(this);
}
~SomeClass()
{
ReleaseHandle(_someHandle);
}
}

如果一个对象已经被传递给GC.SuppressFinalize(),那么步骤4和后续步骤就不会发生。

实现IDisposable的第二种情况是,将IDisposable对象作为"拥有"它(控制它的生存期)的另一个对象的字段:

public class SomeOtherClass : IDisposable
{
private SomeClass _someObj;
public SomeOtherClass(string someIdentifier)
{
_someObj = new SomeClass(someIdentifier);
}
public void Dispose()
{
//If base type is disposable
//call `base.Dispose()` here too.
_someObj.Dispose();
}
}

因此,清理SomeOtherClass意味着清理它作为字段的SomeClass。请注意,我们这里有而不是这里有一个最终确定者。我们不需要一个最终确定者,因为这与我们无关;往好了说,它什么都不做,只是有上面提到的最终确定者的缺点,往坏了说,他会试图清理_someObj,而不知道这是在_someObj清理自己之前还是之后发生的,并且_someObj排队清理自己,在这种情况下,它可以假设没有其他东西可以进行清理。

对于第三种情况,请考虑是否将这两种情况与一个类结合起来,该类同时具有它释放的非托管资源和一个一次性类字段。在这里,如果我们是Dispose()d,我们希望同时清理两者,但如果我们最终确定,我们只希望清理直接处理的非托管资源:

public sealed class SomeHybridClass : IDisposable
{
private IntPtr _someHandle;
private SomeClass _someObj;
public SomeHybridClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
_someObj = new SomeClass(someIdentifier);
}
public void Dispose()
{
ReleaseHandle(_someHandle);
GC.SuppressFinalize(this);
_someObj.Dispose();
}
~SomeHybridClass()
{
ReleaseHandle(_someHandle);
}
}

现在,由于这里有重复,将它们重构为相同的方法是有意义的:

public sealed class SomeHybridClass : IDisposable
{
private IntPtr _someHandle;
private SomeClass _someObj;
public SomeHybridClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
_someObj = new SomeClass(someIdentifier);
}
private void Dispose(bool disposing)
{
if(disposing)
{
_someObj.Dispose();
}
ReleaseHandle(_someHandle);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~SomeHybridClass()
{
Dispose(false);
}
}

对于第四种情况,想象一下,如果这个类没有密封;它的派生类型也需要能够进行这种清理,所以我们使参数化的Dispose(bool)方法受到保护:

public class SomeHybridClass : IDisposable
{
private IntPtr _someHandle;
private SomeClass _someObj;
public SomeHybridClass(string someIdentifier)
{
_someHandle = GetHandle(someIdentifier);
_someObj = new SomeClass(someIdentifier);
}
protected virtual void Dispose(bool disposing)
{
// if this in turn was derived, we'd call
// base.Dispose(disposing) here too.
if(disposing)
{
_someObj.Dispose();
}
ReleaseHandle(_someHandle);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~SomeHybridClass()
{
Dispose(false);
}
}

然而,最后两个例子确实解决了错误的问题:它们解决了如何使一个类同时具有可丢弃类型作为字段和非托管资源,和/或成为类型层次结构的一部分的问题。真的,你最好不要陷入这种境地;要么有一个类处理非托管资源(并且是sealed),要么在字段中具有可丢弃类型,最终只处理前两种情况。如果您通过从SafeHandle派生来处理非托管资源,那么您实际上只需要担心第二种情况,这也会管理一些困难的边缘情况。

实际上,最终确定者应该非常非常少地被写出来,当他们被写出来的时候,他们应该写得尽可能简单,因为他们和他们周围的边缘案例本身就有足够的复杂性。你需要知道如何处理覆盖protected virtual void Dispose(bool disposing)(注意,永远不应该公开),以处理那些对某人来说似乎是个好主意的遗留问题,但不具有同时具有非托管和托管可支配资源的可继承类,从而迫使其他人处于该位置。

如何在Dispose方法中释放Base的属性?(_String,classe1,foo)

现在应该清楚的是,这些字段(属性在.NET中是一个非常不同的东西)不需要释放。它们拥有的唯一资源是托管内存,因此一旦无法访问它们(不在静态中,不打算在方法中对它们做任何事情,也不在其中任何一个类别中的某个字段或其中任何一个中某个字段中某个字段的某个域中,等等),它们的内存将在需要时自动回收。

最新更新