Почему реализация IDisposable спроектирована так, как она

Посмотрим на печально известный интерфейс 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 лучше, потому что я сам этого не понимаю.

Итак, мне интересно, что привело к таким необычным дизайнерским решениям (делая производный класс переопределять другой метод, чем тот, который находится в интерфейсе, и заставлять его думать о неуправляемых ресурсах, которые он, скорее всего, не содержит). Любые мысли по этому вопросу?

Ответ 1

Итак, мне интересно, что привело к таким необычным дизайнерским решениям (делая производный класс переопределять другой метод, чем тот, который находится в интерфейсе, и заставлять его думать о неуправляемых ресурсах, которые он, скорее всего, не содержит). Любые мысли по этому вопросу?

Основная проблема заключается в том, что IDisposable был определен ПОСЛЕ того, как структура уже была разработана и существует. Он предназначен для обработки ситуации, которую управляемый код пытается избежать - так что это действительно крайний случай, если он очень распространенный.;)

Это можно увидеть, кстати, если вы посмотрите на С++/CLI. Он был разработан после IDisposable и, как результат, реализует IDisposable более естественным образом (деструкторы [ ~ClassName] автоматически становятся Dispose, а финализаторы [!ClassName] рассматриваются как финализаторы).

Другая проблема заключается в том, что IDisposable обрабатывает несколько ситуаций. Я написал целую серию блога, прогуливаясь по различным реализациям, которые следует использовать при обновке собственного кода, инкапсулируя класс, реализующий IDisposable, и используя его с факторизованные типы.

Технически, вы < должны реализовывать интерфейс напрямую. Конструктивное решение, позволяющее использовать метод protected virtual void Dispose(bool disposing), допускает дополнительную гибкость, которая не будет легко и безопасно обрабатываться в открытом интерфейсе.

Ответ 2

Я обычно вынимаю догадки для производных классов. Вот мой файл .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

Ответ 3

Ответ на этот и многие другие вопросы проектирования API можно найти в этой книге.

Руководства по дизайну рамок: соглашения, идиомы и шаблоны для многоразовых библиотек .NET http://www.amazon.com/gp/product/0321545613?ie=UTF8&tag=bradabramsblo-20&link_code=wql&camp=212361& творческий = 380601

Это буквально набор правил, которые сотрудники Microsoft используют для создания .NET API. Правила бесплатны (см. Ниже), но в книге есть комментарий, который объясняет правила. Это действительно необходимо для разработчиков .NET.

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

Ответ 4

Я бы сказал, что это better:

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

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

  ~DisposableClass() {
    CleanUpNativeResources();
  }
}

Ответ 5

Журнал MSDN содержит статью об этом шаблоне.

Это не совсем ответит на вопрос, но вы можете использовать следующий фрагмент кода для реализации шаблона.

<?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>

Ответ 6

Я понимаю, что вся причина IDisposable заключается в том, чтобы освободить неуправляемые ресурсы, поэтому я смущен, почему вы заявляете, что "99% пользовательских классов не имеют ничего неуправляемого для распоряжения" - если вы реализуете IDisposable, это должно потому что у вас есть неуправляемые ресурсы.

MSDN IDisposable

Ответ 7

Одна проблема, которую я вижу с вашей реализацией, заключается в том, что для производного класса есть повод не вызывать метод Dispose базового класса. В этом случае GC.SuppressFinalize может не вызываться, когда он должен, и вы в конечном итоге также вызываете Finalizer. Мне нравится решение Will для обеспечения вызова GC.SuppressFinalize. Рекомендуемый способ MSDN имеет сходное чувство и гарантирует, что GC.SuppressFinalize вызывается, если объект Disposed разработчиком.

Ответ 8

Полезной особенностью рекомендуемого шаблона IDisposable является то, что он допускает согласованный шаблон для производных типов, расширяющих типы, реализующие IDisposable, независимо от того, предоставляет ли базовый тип открытый метод без параметров Dispose. Это действительно не так уж плохо шаблон, если рассматривать параметр как манекен, который используется просто, чтобы дать защищенному методу другую подпись из беззаботного Dispose(); самая большая слабость заключается в том, что защита от избыточного Dispose не выполняется поточно-безопасным способом.

Не столь полезная особенность рекомендуемого шаблона IDisposable заключается в том, что он поощряет использование финализаторов/деструкторов во многих случаях, когда они не подходят. Очень редко класс, который происходит от чего-либо другого, кроме System.Object, имеет финализатор для очистки (классы могут иметь финализаторы для того, чтобы регистрировать неспособность правильно распоряжаться). Если класс содержит ссылки на многие управляемые объекты, а также содержит неуправляемый ресурс, неуправляемый ресурс должен быть перенесен в собственный класс-оболочку, превратив его в управляемый ресурс. Этот класс-оболочка может либо выводиться из чего-то вроде SafeHandle, либо может выводиться из Object и определять finalizer/destructor; любой курс действий устранит необходимость в финализаторе/деструкторе в основном классе.