在构造函数中创建的IDisposable对象何时调用Dispose()方法?

14

如何为对象拥有的 IDisposable 对象调用 Dispose() 方法?

public class MyClass
{
    public MyClass()
    {
        log = new EventLog { Source = "MyLogSource", Log = "MyLog" };
        FileStream stream = File.Open("MyFile.txt", FileMode.OpenOrCreate);
    }


    private readonly EventLog log;
    private readonly FileStream stream;

    // Other members, using the fields above
}

对于这个例子,我应该实现Finalize()吗?如果我根本不实现它会有什么问题吗?

我的第一反应是MyClass应该实现IDisposable。但是在MSDN文章中的以下语句让我感到困惑:

只有在直接使用非托管资源时才实现IDisposable。 如果您的应用程序仅使用实现IDisposable的对象,请勿提供IDisposable实现。

这个说法错了吗?


根据您的MSDN更新,您正在直接使用非托管资源。通过将它们作为字段保留,您拥有这些资源,因此需要负责处理它们。如果您仅使用它们(例如将其作为对象传递给方法),则不应实现“IDisposable”。 - Tim Schmelter
我同意MSDN的引述是误导性的。它谈到了“你的应用程序”和一个“应用程序”(不管那是什么)不能实现IDisposable接口,只有类才能实现。 - adrianm
4
该代码并未直接使用非托管资源;它正在获取和使用托管资源(托管资源包装器),这些资源在理论上将被清理,如果被遗弃,则托管资源包装器将持有非托管资源。Microsoft 关于资源的术语相当混乱。 - supercat
3个回答

27
如果MyClass拥有一个IDisposable资源,那么MyClass本身应该是IDisposable,并且在调用MyClassDispose()方法时应该释放封装的资源。
public class MyClass : IDisposable {
    // ...
    public virtual void Dispose() {
        if(stream != null) {
            stream.Dispose();
            stream = null;
        }
        if(log != null) {
            log.Dispose();
            log = null;
        }
    }
}

不,你不应该在这里实现终结器。

注意:另一种实现方式可能是:

private static void Dispose<T>(ref T obj) where T : class, IDisposable {
    if(obj != null) {
        try { obj.Dispose(); } catch {}
        obj = null;
    }
}

public virtual void Dispose() {
    Dispose(ref stream);
    Dispose(ref log);
}

如果设计按照您所建议的进行,并且MyClass实现了IDisposable,那么对于不调用Dispose的用户,同样应该在同一个MyClass中实现finalizer。这将有助于防止内存泄漏,或者我可能遗漏了什么。最终化只用于非托管资源清理吗? - Mrinal Kamboj
2
@MrinalKamboj 是的,你漏掉了一些东西。终结器(finalizers)应该操作非托管资源。它们不能操作托管资源,因为收集顺序是不确定的:stream / log 对象可能已被处理。仅仅需要在 dispose 中执行某些操作。坦白地说,你几乎从不需要添加终结器(finalizer)。 - Marc Gravell
1
@AndrejAdamenko 哇,是的,那个指导完全是错的;听起来像是从“finalizer”页面复制/粘贴过来的。对于 IDisposable,关键在于:谁拥有生命周期?在这种情况下,肯定是 MyClass - Marc Gravell
@AndrejAdamenko,.NET 4指南中没有这个错误:http://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.100).aspx - Marc Gravell
@ Marc Gravell,谢谢,看起来涉及PInvoke、ComInterop的调用将是终结器的理想候选对象,因为它们超越了托管边界。 - Mrinal Kamboj
显示剩余8条评论

1
许多关于DisposeFinalize的建议都是由那些期望Finalize作为主要资源清理机制可行的人编写的。实践表明,这种期望过于乐观。获取任何资源并在方法调用之间保留这些资源的面向公众的对象应该实现IDisposable,并且不应该覆盖Finalize。如果一个对象持有任何资源,如果它被放弃,这些资源将不会被清理,那么它应该将每个这样的资源封装在一个私有对象中,然后如果需要,使用Finalize来清理该资源。
请注意,一个类通常不应该使用Finalize来清理其他对象持有的资源。如果Finalize在持有对其他对象引用的对象上运行,则通常会出现以下几种情况之一:
  • 没有其他引用指向另一个对象,且该对象已经运行了Finalize方法,因此该对象不需要清理任何内容。
  • 没有其他引用指向另一个对象,且该对象尚未运行Finalize方法但已计划运行,因此该对象不需要清理任何内容。
  • 其他某个对象仍在使用该对象,因此该对象不应尝试清理它。
  • 无法从终结器线程上下文中安全地运行另一个对象的清理方法,因此该对象不应尝试清理它。
  • 该对象之所以成为运行Finalize方法的候选对象,是因为所有必要的清理工作都已完成,因此该对象不需要清理任何内容。

只有在了解上述情况均不适用的情况下才定义Finalize方法。虽然这样的情况存在,但它们很少见,而且没有Finalize方法比有一个不合适的方法更好。


1
对于包含其他IDisposable对象的对象,实现IDisposable在自己的对象上是一种好的建议性做法, 这样使用你的类型的其他人就可以将其包装在using语句中:
public class MyClass : IDisposable
{
    public MyClass()
    {
        log = new EventLog { Source = "MyLogSource", Log="MyLog" };
        FileStream stream = File.Open("MyFile.txt", FileMode.OpenOrCreate);
    }


    private readonly EventLog log;
    private readonly FileStream stream;

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Free managed objects here
            stream.Dispose();
        }
    }

    // Other members, using the fields above
}

在您的情况下,您没有释放任何托管资源,因此不需要使用终结器。如果您有托管资源,则应实现终结器并调用Dispose(false),表示您的处理方法正在从终结器线程中运行。
如果您不实现IDisposable,则将其留给GC来清理资源(例如,关闭您已打开的FileStream上的Handle)一旦GC进行收集。假设您的MyClass对象符合收集条件,当前处于第1代。您将保持FileStream句柄处于打开状态,直到GC运行并清除资源为止。此外,许多Dispose的实现调用GC.SuppressFinalize以避免您的对象在初始化队列到达F-可达队列时再次生存另一个GC周期。

谢谢您的回答。我对MSDN文档感到困惑,可以看一下我对问题的更新吗?难道MSDN错了吗? - Andrej Adamenko
FileStream会在内部分配未托管的资源。它将打开一个FileHandle,如果MyClass的用户没有调用Dispose,那么这不需要清理吗? - Mrinal Kamboj
它确实会。正如我所说,如果您不调用Dispose,它将由FileStream的终结器实现来运行并关闭打开的句柄。 - Yuval Itzchakov
@AndrejAdamenko 我认为那个指南并不相关,而且我也不确定它是否更新了。 - Yuval Itzchakov
1
@YuvalItzchakov 疯狂的事情是:这个不靠谱的指导并没有在.NET 4.0版本中出现;我认为有人错误地将“finalizer”指导复制/粘贴到了IDisposable页面中。叹气。 - Marc Gravell
@Yuval Itzchakov,好的,谢谢。即使是FileStream,也会有一个终结器来处理非托管内存。 - Mrinal Kamboj

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