Microsoft IDisposable 模式真的正确吗?

Is Microsoft IDisposable pattern actually correct?

我多次偶然发现 Microsoft 推荐的实现 IDisposable 模式的方法,它甚至作为 lamp 图标菜单中的“实现接口”选项出现在 Visual Studio 中。它看起来像这样:

// Override only if 'Dispose(bool disposing)' has code to free unmanaged resources
~Foo() {
    // Do not change this code.
    Dispose(calledByFinalizer: true);
}
public void Dispose() {
    // Do not change this code. 
    Dispose(calledByFinalizer: false);
    GC.SuppressFinalize(this);
}
// Put cleanup code here
protected virtual void Dispose(bool calledByFinalizer) {
    if (_disposed) return;

    if (!calledByFinalizer) { /* dispose managed objects */ }

    /* free unmanaged resources and set large fields to null */

    _disposed = true;
}

我稍微重构了建议的代码(因为 Dispose(bool disposing) 会伤人的脑子,而嵌套的 if 会伤人的眼睛)。

但我还有一些疑问:

  1. 假定该方法将被调用一次。那为什么 _disposed = true 放在方法的末尾而不是开头呢?如果 IDisposable.Dispose() 是从不同的线程调用的,那么它们都可以绕过 if (_disposed) return; 检查并实际执行两次方法体。为什么不这样做:
    if (_disposed) return;
    else _disposed = true;
  1. 为什么 protected virtual void Dispose(bool disposing) 标记为 virtual?任何派生的 class 都无法访问 _disposed 字段,并且很容易破坏其行为。我们只能将派生的 class 可以在不调用 base.Dispose():
  2. 的情况下执行任何操作的可选部分标记为 virtual
~Foo() => FreeUnmanagedResources();

public void Dispose() {
    if (_disposed) return;
    else _disposed = true;

    DisposeManagedObjects();
    FreeUnmanagedResources();

    GC.SuppressFinalize(this);
}

protected virtual void DisposeManagedObjects() { }
protected virtual void FreeUnmanagedResources() { }
  1. 您不能假设 Dispose 只会被调用一次。在最佳实践中,是的。在最坏的情况下,根本不会。并不是每一种情况都可以方便地使用 using 语句。因此,与其冒着代码尝试两次清理非托管资源的风险——这可能会变得非常糟糕,具体取决于资源的类型——添加了一个标志来阻止它。就记住 dispose 是否已经被调用而言,这减轻了调用代码的负担。

  2. Dispose 必须声明为虚拟以支持任何单独的清理,如果子class 是创建实例化与基础 class 中使用的资源 截然不同 的任何非托管资源。 subclass 中的 Dispose 应该在清理自己的混乱之前或之后调用 base.Dispose();

指南是正确的,是公认的最佳实践,并在此处进行了完整记录:Implement a Dispose method

规则 1,不要重构处置模式,它有明确的注释,详细说明在何处执行每个常见操作,并且终结器默认被注释掉(或应该被注释掉),只有在您有unmanaged 资源释放。随意删除您不需要的元素和注释,但如果您坚持我们都知道和期望的模式,它有助于使您的代码更易于访问。

Visual Studio 将为您提供实施 Dispose 模式 的建议,如以下屏幕截图所示:

此模式的最佳实践是这样的,但我个人更喜欢将其包装在区域块中:

public class T : IDisposable
{
    #region IDisposable

    private bool disposedValue;

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            disposedValue = true;
        }
    }

    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
    // ~T()
    // {
    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    #endregion IDisposable
}

It is assumed that the method will be called once.

是的,我们都做出了这个假设,但由于程序员的判断力、线程和普遍的不良设计模式,我们都假设某些 nufty 会违反我们的规则。通常它是在关闭时触发的 UI 中直接调用 dispose 的一种激进形式,但它也发生在许多其他场景中。因此,强烈建议提供的模式满足多次调用 dispose 的场景。

Why is protected virtual void Dispose(bool disposing) flagged as virtual? Any derived class does not have access to the _disposed field...

所以这个方法存在并且是虚拟的原因具体是因为继承 class 无法访问 _disposed 字段,重写实现 应该 调用基础实现:

base.Dispose(disposing):

如果他们不这样做,那么他们就会像您所说的那样破坏预期的功能,但这是他们的特权,在某些有效的情况下,我们确实特别想要更改实现,这就是我们覆盖的原因。

作为 UI 控件作者,此模式非常有用,并且是一种很好的封装级别。在像你这样的情况下,大多数最终会覆盖 DisposeManagedObjectsFreeUnmanagedResources。通过分离这些关注点,您很难执行我们可能也想执行的任何其他 C# Kung Fu。


您的实现不错,在您的用例中可以说更安全,但它是您独有的,并且确实使一些高级任务更难实现或维护,因为现在处理代码分为 2 个方法。

通过使用 标准 模式,其他开发人员和开发工具更有可能从本质上理解您的 classes 并与之交互。这种模式已经发展了很多年,是我们过去如何以自己古怪的方式做事的集合。

处置模式 很难理解,除非您需要重写它,否则很难看出此实现中的价值。但是当它出错时,调试起来非常困难,因为它通常是我们最后看的地方(我们总是假设它有效)但它也很难在 using(){} 块之外跟踪。

使用标准,那些继承你的代码或运行以后维护它的人会感谢你。


该模式是正确的,但假设了最坏的情况,您还必须实施终结器。也就是说,如果您需要一个终结器,您还必须遵循整个模式。然而...

您通常根本不需要终结器。

只有在为非托管资源创建原始托管包装器时才需要终结器。

例如,假设您创建了一个全新的、前所未见的数据库系统。您希望为这种新型数据库提供 .Net ADO 提供程序,包括连接(它将继承自 Dbconnection)。这里的底层网络操作将是一个非托管资源,并且还没有一个终结器来在你的继承树中的任何地方释放它们。因此,您必须实现自己的终结器。

另一方面,如果您正在为您的应用程序创建一个包装器对象来管理与现有数据库类型的连接——只是重新打包(包装或继承)现有的 SqlConnection、OleDbConnection、MySqlConnection 等——那么你仍然应该实现 IDisposable,但是已经为非托管资源提供了一个终结器,您不需要再写一个。

事实证明,当您没有终结器时,您可以安全地从记录的 IDisposable 模式中删除大量代码。