在我的终结器中处理我的 System.IDisposable 对象

Disposing my System.IDisposable object in my finalizer

如果我的对象管理其他实现 System.IDisposable.

的托管对象,Whosebug 上有几个关于该怎么做的讨论

注:下面我不是说的是非托管代码。我完全理解清理非托管代码的重要性

大多数讨论都说,如果您的对象拥有另一个实现 System.IDisposable 的托管对象,那么您也应该实现 System.IDisposable,在这种情况下,您应该调用 Dispose()您的对象持有的一次性物品。这是合乎逻辑的,因为您不知道您拥有的一次性对象是否使用非托管代码。您只知道另一个对象的创建者认为如果您不再需要该对象就立即调用 Dispose 是明智的。

Whosebug 上对 Disposable 模式进行了很好的解释,由社区 wiki 编辑:

Proper use of the IDisposable interface

经常,而且在提到的link中我读到:

"You don't know the order in which two objects are destroyed. It is entirely possible that in your Dispose() code, the managed object you're trying to get rid of is no longer there."

这让我很困惑,因为我认为只要任何对象持有对对象 X 的引用,那么对象 X 就不会也不能被终结。

或者换句话说:只要我的对象持有对对象 X 的引用,我就可以确定对象 X 没有完成。

如果这是真的,那为什么会这样,如果我持有对我的对象的引用直到我完成,我引用的对象已经完成了?

事实介于两者之间:

  • 无法对对象进行垃圾回收,因此对象不再"being there"的可能性不成立
  • 一个对象可以当不再有来自其他不可终结对象的任何引用时被终结。

如果对象 X 引用对象 Y,但两者都是可终结的,那么对象 Y 完全有可能在对象 X 之前终结,甚至它们可以同时终结。

如果您的假设是正确的,那么您可以创建两个相互引用的对象(并具有终结器),并且它们 永远不会 被垃圾收集,因为它们永远不会被垃圾收集已完成。

如果处理得当,您不必担心处理已经处理过的对象。 Dispose 的每个实现如果之前已被处置,则不应执行任何操作。

所以实际上,您无法知道是否有任何子对象已被处置或已经完成(因为完成的顺序是随机的,请参阅其他 post),但无论如何您都可以安全地调用它们的 Dispose 方法。

引用 Eric Lippert's, When everything you know is wrong, part two

Myth: Keeping a reference to an object in a variable prevents the finalizer from running while the variable is alive; a local variable is always alive at least until control leaves the block in which the local was declared.

{   
     Foo foo = new Foo();
     Blah(foo);  // Last read of foo
     Bar();
     // We require that foo not be finalized before Bar();
     // Since foo is in scope until the end of the block,
     // it will not be finalized until this point, right?
}

The C# specification states that the runtime is permitted broad latitude to detect when storage containing a reference is never going to be accessed again, and to stop treating that storage as a root of the garbage collector. For example, suppose we have a local variable foo and a reference is written into it at the top of the block. If the jitter knows that a particular read is the last read of that variable, the variable can legally be removed from the set of GC roots immediately; it doesn’t have to wait until control leaves the scope of the variable. If that variable contained the last reference then the GC can detect that the object is unreachable and put it on the finalizer queue immediately. Use GC.KeepAlive to avoid this.

Why does the jitter have this latitude? Suppose the local variable is enregistered into the register needed to pass the value to Blah(). If foo is in a register that Bar() needs to use, there’s no point in saving the value of the never-to-be-read-again foo on the stack before Bar() is called. (If the actual details of the code generated by the jitter is of interest to you, see Raymond Chen’s deeper analysis of this issue.)

Extra bonus fun: the runtime uses less aggressive code generation and less aggressive garbage collection when running the program in the debugger, because it is a bad debugging experience to have objects that you are debugging suddenly disappear even though the variable referring to the object is in scope. That means that if you have a bug where an object is being finalized too early, you probably cannot reproduce that bug in the debugger!

See the last point in this article for an even more horrid version of this problem.

在所有答案之后,我创建了一个小程序来显示 Jodrell 写的内容(谢谢 Jodrell!)

  • 一个对象只要不被使用就可以被垃圾回收,即使我有对它的引用
  • 只有在不调试的情况下才会这样做。

我写了一个简单的 class 来分配非托管内存和 MemoryStream。后一个实现 System.IDisposable.

根据 Whosebug 上的每个人的说法,如果调用我的 Dispose,我应该实现 System.IDisposable 并释放非托管内存以及 Dispose 托管内存流,但如果调用我的终结器,我应该只释放非托管内存。

我写了一些诊断控制台消息

class ClassA : System.IDisposable
{
    IntPtr memPtr = Marshal.AllocHGlobal(1024);
    Stream memStream = new MemoryStream(1024);
        
    public ClassA()
    {
        Console.WriteLine("Construct Class A");
    }
    
    ~ClassA()
    {
        Console.WriteLine("Finalize Class A");
        this.Dispose(false);
    }
    
    public void Dispose()
    {
        Console.WriteLine("Dispose()");
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    public void Dispose(bool disposing)
    {
        Console.WriteLine("Dispose({0})", disposing.ToString());
        if (!this.IsDisposed)
        {
            if (disposing)
            {
                Console.WriteLine("Dispose managed objects");
                memStream.Dispose();
            }

            Console.WriteLine("Dispose unmanaged objects");
            Marshal.FreeHGlobal(memPtr);                
        }
    }

    public bool IsDisposed { get { return this.memPtr == null; } }
}

该程序遵循多次描述的 Dispose 模式,a.o。在 Proper use of the IDisposable interface

的 Whosebug 中

顺便说一句:为简单起见,我省略了异常处理

一个简单的控制台程序创建对象,不使用它,但保留对它的引用并强制垃圾收集器收集:

private static void TestFinalize()
{
    ClassA a = new ClassA() { X = 4 };

    Console.WriteLine("Start Garbage Collector");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("Done");
}

请注意,变量 a 持有对对象的引用,直到过程结束。我忘了处理,所以我的终结器应该处理处理

从你的 main 调用这个方法。 运行 从调试器构建(发布)并通过命令提示符 运行 它。

  • 如果 运行 来自调试器,那么该对象将保持活动状态直到过程结束,因此直到垃圾收集器完成收集之后
  • 如果 运行 来自命令提示符,则对象在过程结束前完成,即使我仍然有对该对象的引用。

So Jodrell is right:

  • unmanaged code needs Dispose() and Finalize, use Dispose(bool)

  • Managed disposable objects need Dispose(), preferably via Dispose(bool). In Dispose(bool) only call Dispose() of managed objects if disposing

  • don't trust the debugger: it makes that objects are finalized on different moments than without debugger

在大多数情况下,当对一个包含一个或多个 IDisposable 对象的引用的对象调用 Finalize 时,将应用以下一项或多项:

  1. 另一个对象已经被清理了,这种情况下调用Dispose最多也没用。
  2. 另一个对象已计划尽快完成,但尚未完成,在这种情况下调用 Dispose 可能是不必要的。
  3. 另一个对象的清理代码不能在终结器线程上下文中安全使用,在这种情况下调用 Dispose 可能是灾难性的。
  4. 其他对象仍在被其他地方的代码使用,在这种情况下调用 Dispose 可能是灾难性的。

在某些情况下,代码对它正在处理的 IDisposable 对象有足够的了解,可以知道上述 none 是否适用,或者尽管有上述情况,它仍应触发清理;然而,通过让其他对象提供一种方法 而不是 Dispose 被最终确定的对象可以调用的方法,可能会更好地满足这些情况。