为什么IDisposable实现的设计方式如此

时间:2010-01-25 18:56:05

标签: c# .net

让我们来看看臭名昭着的IDisposable界面:

[ComVisible(true)]
public interface IDisposable
{
    void Dispose();
}

和MSDN推荐的典型实现(我省略了检查当前对象是否已被处理):

public class Base : IDisposable
{
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release managed
        }
        // release unmanaged
        disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Base()
    {
        Dispose(false);
    }
}

public class Derived : Base
{
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
        {
            // release managed
        }
        // release unmanaged
        disposed = true;
    }
}

问题是:我认为这种实现是违反直觉的。它在基类和派生类中也有显着差异。派生类应该假设该基类正确实现了IDisposable,然后重写Dispose(bool),它甚至不是原始接口的一部分。

我不得不承认,我想出了这个问题,因为我经常要求初级程序员在求职面试中实施IDisposable。如果他们不确切地知道应该怎么做,他们会提出一些接近这个的东西:

public class Base : IDisposable
{
    public virtual void Dispose()
    {
        // release managed and unmanaged
        GC.SuppressFinalize(this);
    }

    ~Base()
    {
        // release unmanaged
    }
}

public class Derived : Base
{
    public override void Dispose()
    {
        // release managed and unmanaged
        base.Dispose();
    }

    ~Derived()
    {
        // release unmanaged
    }
}

对我而言,这种实施更加清晰,更加一致。当然,最糟糕的是我们必须在两个不同的地方释放非托管资源,但重要的是,可能超过99%的自定义类没有任何不受管理的东西,因此无论如何它们都不需要终结器。我无法向初级程序员解释为什么MSDN实现更好,因为我自己并不了解它。

所以我想知道,是什么导致了这种不同寻常的设计决策(使派生类覆盖与界面中的方法不同的方法,并让他考虑非常可能不包含的非托管资源)。对此事有何想法?

8 个答案:

答案 0 :(得分:12)

我通常会对派生类进行猜测。这是我的.snippet文件:

#region IDisposable pattern
/// <summary>
/// Dispose of (clean up and deallocate) resources used by this class.
/// </summary>
/// <param name="fromUser">
/// True if called directly or indirectly from user code.
/// False if called from the finalizer (i.e. from the class' destructor).
/// </param>
/// <remarks>
/// When called from user code, it is safe to clean up both managed and unmanaged objects.
/// When called from the finalizer, it is only safe to dispose of unmanaged objects.
/// This method should expect to be called multiple times without causing an exception.
/// </remarks>
protected virtual void Dispose(bool fromUser)
    {
    if (fromUser)   // Called from user code rather than the garbage collector
        {
        // Dispose of managed resources (only safe if called directly or indirectly from user code).
        try
            {
      DisposeManagedResources();
            GC.SuppressFinalize(this);  // No need for the Finalizer to do all this again.
            }
        catch (Exception ex)
            {
            //ToDo: Handle any exceptions, for example produce diagnostic trace output.
            //Diagnostics.TraceError("Error when disposing.");
            //Diagnostics.TraceError(ex);
            }
        finally
            {
            //ToDo: Call the base class' Dispose() method if one exists.
            //base.Dispose();
            }
        }
    DisposeUnmanagedResources();
    }
/// <summary>
/// Called when it is time to dispose of all managed resources
/// </summary>
  protected virtual void DisposeManagedResources(){}
/// <summary>
/// Called when it is time to dispose of all unmanaged resources
/// </summary>
  protected virtual void DisposeUnmanagedResources(){}
/// <summary>
/// Dispose of all resources (both managed and unmanaged) used by this class.
/// </summary>
public void Dispose()
    {
    // Call our private Dispose method, indicating that the call originated from user code.
    // Diagnostics.TraceInfo("Disposed by user code.");
    this.Dispose(true);
    }
/// <summary>
/// Destructor, called by the finalizer during garbage collection.
/// There is no guarantee that this method will be called. For example, if <see cref="Dispose"/> has already
/// been called in user code for this object, then finalization may have been suppressed.
/// </summary>
~$MyName$()
    {
    // Call our private Dispose method, indicating that the call originated from the finalizer.
    // Diagnostics.TraceInfo("Finalizer is disposing $MyName$ instance");
    this.Dispose(false);
    }
#endregion

答案 1 :(得分:7)

  

所以我想知道,是什么导致了这种不同寻常的设计决策(使派生类覆盖与界面中的方法不同的方法,并让他考虑非常可能不包含的非托管资源)。对此事有何想法?

主要问题是IDisposable是在框架已经设计并存在之后定义的。它旨在处理托管代码试图避免的情况 - 所以它实际上是一个边缘情况,如果是一个非常常见的情况。 ;)

如果你看一下C ++ / CLI,可以看到这个。它是在IDisposable之后设计的,因此,以更自然的方式实现IDisposable(析构函数[~ClassName]自动变为Dispose,终结器[!ClassName]被视为终结器)。

另一个问题是IDisposable处理多种情况。我编写了一个entire blog series,介绍了在包装本机代码时应该使用的不同实现,封装了一个实现IDisposable的类,并将其与因式类型一起使用。

从技术上讲,您只有必须直接实现该界面。允许protected virtual void Dispose(bool disposing)方法的设计决策允许在公共界面中不容易且安全地处理的额外灵活性。

答案 2 :(得分:2)

本书和大多数其他API设计问题的答案可以在本书中找到。

框架设计指南:可重用.NET库的约定,惯用语和模式 http://www.amazon.com/gp/product/0321545613?ie=UTF8&tag=bradabramsblo-20&link_code=wql&camp=212361&creative=380601

这实际上是Microsoft员工用于构建.NET API的一组规则。规则是免费的(见下文),但本书有解释规则的评论。它确实是.NET开发人员的必备条件。

http://msdn.microsoft.com/en-us/library/b1yfkh5e.aspx

答案 3 :(得分:2)

我说这是better

public class DisposableClass : IDisposable {
  void IDisposable.Dispose() {
    CleanUpManagedResources();
    CleanUpNativeResources();
    GC.SuppressFinalize(this);
  }

  protected virtual void CleanUpManagedResources() { 
    // ...
  }
  protected virtual void CleanUpNativeResources() {
    // ...
  }

  ~DisposableClass() {
    CleanUpNativeResources();
  }
}

答案 4 :(得分:1)

MSDN杂志有article about this pattern

这不能完全回答这个问题,但您可以使用以下代码段来实现该模式。

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Dispose pattern</Title>
            <Shortcut>dispose</Shortcut>
            <Description>Code snippet for virtual dispose pattern</Description>
            <Author>SLaks</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal Editable="false">
                    <ID>classname</ID>
                    <ToolTip>Class name</ToolTip>
                    <Default>ClassNamePlaceholder</Default>
                    <Function>ClassName()</Function>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[
        ///<summary>Releases unmanaged resources and performs other cleanup operations before the $classname$ is reclaimed by garbage collection.</summary>
        ~$classname$() { Dispose(false); }
        ///<summary>Releases all resources used by the $classname$.</summary>
        public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
        ///<summary>Releases the unmanaged resources used by the $classname$ and optionally releases the managed resources.</summary>
        ///<param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing) {
            if (disposing) {
                $end$$selected$
            }
        }]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

答案 5 :(得分:0)

我的理解是IDisposable的全部原因是释放非托管资源,所以我很困惑为什么你说“99%的自定义类没有任何不受管理的东西” - 如果你正在实现IDisposable它应该因为你有非托管资源。

MSDN IDisposable

答案 6 :(得分:0)

我在您的实现中看到的一个问题是派生类有可能不调用基类Dispose方法。在这种情况下,GC.SuppressFinalize可能不会被调用,你最终也会调用Finalizer。我喜欢Will的解决方案,以确保调用GC.SuppressFinalize。 MSDN推荐的方式具有类似的感觉,并确保在开发人员处置对象时调用GC.SuppressFinalize。

答案 7 :(得分:0)

推荐的IDisposable模式的一个有用特性是它允许派生类型的一致模式扩展实现IDisposable的类型,,而与基类型是否公开名为Dispose 的公共无参数方法无关。如果将参数视为一个虚拟对象,它只是用于为受保护的方法提供与无参数Dispose()的不同签名,那么模式真的不是太糟糕。最大的缺点是不能以线程安全的方式执行对冗余Dispose的保护。

推荐的IDisposable模式的一个不那么有用的特性是它鼓励在很多不合适的情况下使用终结器/析构器。从System.Object以外的任何其他类派生的类很少应该有一个用于清理的终结器(类可能有终结器用于记录失败以正确处置)。如果类包含对许多托管对象的引用,并且还包含非托管资源,则应将非托管资源移出到其自己的包装类中,并将其转换为托管资源。该包装类可以从SafeHandle派生,也可以从Object派生并定义终结器/析构函数;任何一个行动都将消除主类中终结者/析构函数的需要。