Следует ли реализовать IDisposable.Dispose (), чтобы он никогда не выдавал? - PullRequest
53 голосов
/ 23 февраля 2009

Для эквивалентного механизма в C ++ (деструктор) совет состоит в том, что обычно не должен выдавать никаких исключений . Это происходит главным образом потому, что таким образом вы можете завершить свой процесс, что очень редко является хорошей стратегией.

В аналогичном сценарии в .NET ...

  1. выдается первое исключение
  2. Блок finally выполняется в результате первого исключения
  3. Блок finally вызывает метод Dispose ()
  4. Метод Dispose () выдает второе исключение

... ваш процесс не прекращается немедленно. Однако вы теряете информацию, потому что .NET безоговорочно заменяет первое исключение вторым. Поэтому блок перехвата где-то в стеке вызовов никогда не увидит первое исключение. Тем не менее, обычно первое больше интересует первое исключение, потому что оно обычно дает лучшие подсказки относительно того, почему что-то пошло не так.

Поскольку в .NET отсутствует механизм для определения того, выполняется ли код во время ожидания исключения, кажется, что на самом деле существует только два варианта реализации IDisposable:

  • Всегда проглатывайте все исключения, возникающие внутри Dispose (). Не очень хорошо, так как вы могли бы в конечном итоге проглотить OutOfMemoryException, ExecutionEngineException и т. Д., Которые я обычно предпочел бы прервать процесс, когда они происходят без другого ожидающего исключения.
  • Пусть все исключения распространяются из Dispose (). Не очень хорошо, так как вы можете потерять информацию об основной причине проблемы, см. Выше.

Итак, какое из двух зол меньше? Есть ли лучший способ?

EDIT : Чтобы уточнить, я не говорю об активном генерировании исключений из Dispose () или нет, я говорю о том, чтобы исключения, генерируемые методами, вызываемыми Dispose (), распространялись из Dispose ( ) или нет, например:

using System;
using System.Net.Sockets;

public sealed class NntpClient : IDisposable
{
    private TcpClient tcpClient;

    public NntpClient(string hostname, int port)
    {
        this.tcpClient = new TcpClient(hostname, port);
    }

    public void Dispose()
    {
        // Should we implement like this or leave away the try-catch?
        try
        {
            this.tcpClient.Close(); // Let's assume that this might throw
        }
        catch
        {
        }
    }
}

Ответы [ 8 ]

36 голосов
/ 23 февраля 2009

Руководство по проектированию платформы (2 nd ed) имеет это как (§9.4.1):

ИЗБЕГАТЬ выдает исключение изнутри Dispose (bool) за исключением критических ситуации, когда содержащий процесс был поврежден (утечки, противоречивые общее состояние и т. д.).

Комментарий [Редактировать]:

  • Есть рекомендации, а не жесткие правила. И это «ИЗБЕГАТЬ», а не «НЕ». Как отмечено (в комментариях), Рамочная основа нарушает это (и другие) руководящие принципы местами. Хитрость заключается в том, чтобы знать, когда нарушать правила. В этом во многом разница между подмастерьем и мастером.
  • Если какая-то часть очистки может завершиться неудачей, следует предоставить метод Close, который будет генерировать исключения, чтобы вызывающий мог обрабатывать их.
  • Если вы следуете шаблону удаления (и вам следует это делать, если тип непосредственно содержит какой-то неуправляемый ресурс), то из финализатора может быть вызван Dispose(bool), бросание из финализатора - плохая идея, и блокирование других объектов завершается.

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

17 голосов
/ 23 февраля 2009

Я бы сказал, что глотание - меньшее из двух зол в этом сценарии, так как лучше поднять оригинал Exception - предостережение: , если не , возможно, неудача чистая утилизация сама по себе является чертовски критичной (возможно, если TransactionScope не может утилизировать, поскольку это может указывать на ошибку отката).

См. здесь , чтобы узнать больше об этом, включая идею метода оболочки / расширения:

using(var foo = GetDodgyDisposableObject().Wrap()) {
   foo.BaseObject.SomeMethod();
   foo.BaseObject.SomeOtherMethod(); // etc
} // now exits properly even if Dispose() throws

Конечно, вы также можете сделать некоторую странность, когда вы повторно сгенерируете составное исключение как с исходным, так и со вторым (Dispose()) исключением - но подумайте: у вас может быть несколько блоков using ... это быстро стать неуправляемым. На самом деле, оригинальное исключение является интересным.

6 голосов
/ 23 февраля 2009

Dispose должен быть предназначен для выполнения своей цели, утилизируя объект. Эта задача безопасна и большую часть времени не вызывает исключений . Если вы видите, что выкидываете исключения из Dispose, вам, вероятно, стоит подумать дважды, чтобы увидеть, делаете ли вы в нем слишком много вещей. Кроме того, я думаю, что к Dispose следует относиться так же, как и ко всем другим методам: обращайтесь, если вы можете что-то с ним сделать, позвольте ему пузыриться, если вы не можете.

РЕДАКТИРОВАТЬ: для указанного примера я написал бы код так, чтобы мой код не вызывал исключение, но очистка TcpClient вверх может вызвать исключение, которое должно быть допустимым чтобы распространять по моему мнению (или обрабатывать и отбрасывать как более общее исключение, как любой метод):

public void Dispose() { 
   if (tcpClient != null)
     tcpClient.Close();
}

Однако, как и любой метод, если вы знаете, tcpClient.Close() может выдать исключение, которое должно быть проигнорировано (не имеет значения) или должно быть представлено другим объектом исключения, вы можете захотеть его перехватить.

2 голосов
/ 17 января 2011

Жаль, что Microsoft не предоставила параметр Exception для Dispose с намерением поместить его в InnerException в случае, если сама утилизация выдает исключение. Безусловно, эффективное использование такого параметра потребовало бы использования блока фильтра исключений, который C # не поддерживает, но, возможно, наличие такого параметра могло бы мотивировать разработчиков C # на предоставление такой функции? Один хороший вариант, который я хотел бы видеть, - это добавление «параметра» исключения в блок «Наконец», например,

  finally Exception ex: // In C#
  Finally Ex as Exception  ' In VB

, который будет вести себя как обычный блок finally, за исключением того, что 'ex' будет иметь значение null / Nothing, если 'Try' завершится, или будет удерживать выброшенное исключение, если это не так. Жаль, что нет способа заставить существующий код использовать такую ​​функцию.

2 голосов
/ 23 февраля 2009

Освобождение ресурсов должно быть «безопасной» операцией - в конце концов, как я могу оправиться от невозможности освободить ресурс? поэтому исключение из Dispose просто не имеет смысла.

Однако, если я обнаружу в Dispose, что состояние программы повреждено, лучше бросить исключение, чем проглотить его, лучше разбить его сейчас, чтобы продолжить работу и выдавать неправильные результаты.

1 голос
/ 25 февраля 2016

Существуют различные стратегии для распространения или проглатывания исключений из метода Dispose, возможно, основанные на том, было ли также выброшено незаполненное исключение из основной логики. Лучшим решением было бы оставить решение до вызывающего абонента, в зависимости от их конкретных требований. Я реализовал общий метод расширения, который делает это, предлагая:

  • по умолчанию using семантика распространения Dispose исключения
  • Предложение Марка Гравелла всегда глотать Dispose исключения
  • альтернатива maxyfc только глотание Dispose исключений, когда есть исключение из основной логики, которое в противном случае было бы потеряно
  • Подход Даниэля Чемберса , заключающийся в объединении нескольких исключений в AggregateException
  • аналогичный подход, который всегда упаковывает все исключения в AggregateException (как Task.Wait делает)

Это мой метод расширения:

/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Executes the specified action delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="action">The action to execute using the disposable resource.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <exception cref="ArgumentNullException"><paramref name="disposable"/> or <paramref name="action"/> is <see langword="null"/>.</exception>
    public static void Using<TDisposable>(this TDisposable disposable, Action<TDisposable> action, DisposeExceptionStrategy strategy)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(action, nameof(action));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        Exception mainException = null;

        try
        {
            action(disposable);
        }
        catch (Exception exception)
        {
            mainException = exception;
            throw;
        }
        finally
        {
            try
            {
                disposable.Dispose();
            }
            catch (Exception disposeException)
            {
                switch (strategy)
                {
                    case DisposeExceptionStrategy.Propagate:
                        throw;

                    case DisposeExceptionStrategy.Swallow:
                        break;   // swallow exception

                    case DisposeExceptionStrategy.Subjugate:
                        if (mainException == null)
                            throw;
                        break;    // otherwise swallow exception

                    case DisposeExceptionStrategy.AggregateMultiple:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw;

                    case DisposeExceptionStrategy.AggregateAlways:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw new AggregateException(disposeException);
                }
            }

            if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
                throw new AggregateException(mainException);
        }
    }
}

Это реализованные стратегии:

/// <summary>
/// Identifies the strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method
/// of an <see cref="IDisposable"/> instance, in conjunction with exceptions thrown by the main logic.
/// </summary>
/// <remarks>
/// This enumeration is intended to be used from the <see cref="DisposableExtensions.Using"/> extension method.
/// </remarks>
public enum DisposeExceptionStrategy
{
    /// <summary>
    /// Propagates any exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// If another exception was already thrown by the main logic, it will be hidden and lost.
    /// This behaviour is consistent with the standard semantics of the <see langword="using"/> keyword.
    /// </summary>
    /// <remarks>
    /// <para>
    /// According to Section 8.10 of the C# Language Specification (version 5.0):
    /// </para>
    /// <blockquote>
    /// If an exception is thrown during execution of a <see langword="finally"/> block,
    /// and is not caught within the same <see langword="finally"/> block, 
    /// the exception is propagated to the next enclosing <see langword="try"/> statement. 
    /// If another exception was in the process of being propagated, that exception is lost. 
    /// </blockquote>
    /// </remarks>
    Propagate,

    /// <summary>
    /// Always swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method,
    /// regardless of whether another exception was already thrown by the main logic or not.
    /// </summary>
    /// <remarks>
    /// This strategy is presented by Marc Gravell in
    /// <see href="http://blog.marcgravell.com/2008/11/dontdontuse-using.html">don't(don't(use using))</see>.
    /// </remarks>
    Swallow,

    /// <summary>
    /// Swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method
    /// if and only if another exception was already thrown by the main logic.
    /// </summary>
    /// <remarks>
    /// This strategy is suggested in the first example of the Stack Overflow question
    /// <see href="https://stackoverflow.com/q/1654487/1149773">Swallowing exception thrown in catch/finally block</see>.
    /// </remarks>
    Subjugate,

    /// <summary>
    /// Wraps multiple exceptions, when thrown by both the main logic and the <see cref="IDisposable.Dispose"/> method,
    /// into an <see cref="AggregateException"/>. If just one exception occurred (in either of the two),
    /// the original exception is propagated.
    /// </summary>
    /// <remarks>
    /// This strategy is implemented by Daniel Chambers in
    /// <see href="http://www.digitallycreated.net/Blog/51/c%23-using-blocks-can-swallow-exceptions">C# Using Blocks can Swallow Exceptions</see>
    /// </remarks>
    AggregateMultiple,

    /// <summary>
    /// Always wraps any exceptions thrown by the main logic and/or the <see cref="IDisposable.Dispose"/> method
    /// into an <see cref="AggregateException"/>, even if just one exception occurred.
    /// </summary>
    /// <remarks>
    /// This strategy is similar to behaviour of the <see cref="Task.Wait()"/> method of the <see cref="Task"/> class 
    /// and the <see cref="Task{TResult}.Result"/> property of the <see cref="Task{TResult}"/> class:
    /// <blockquote>
    /// Even if only one exception is thrown, it is still wrapped in an <see cref="AggregateException"/> exception.
    /// </blockquote>
    /// </remarks>
    AggregateAlways,
}

Пример использования:

new FileStream(Path.GetTempFileName(), FileMode.Create)
    .Using(strategy: DisposeExceptionStrategy.Subjugate, action: fileStream =>
    {
        // Access fileStream here
        fileStream.WriteByte(42);
        throw new InvalidOperationException();
    });   
    // Any Dispose() exceptions will be swallowed due to the above InvalidOperationException

Обновление : Если вам требуется поддержка делегатов, которые возвращают значения и / или являются асинхронными, то вы можете использовать следующие перегрузки:

/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Executes the specified action delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="action">The action delegate to execute using the disposable resource.</param>
    public static void Using<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Action<TDisposable> action)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(action, nameof(action));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        disposable.Using(strategy, disposableInner =>
        {
            action(disposableInner);
            return true;   // dummy return value
        });
    }

    /// <summary>
    /// Executes the specified function delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <typeparam name="TResult">The type of the return value of the function delegate.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="func">The function delegate to execute using the disposable resource.</param>
    /// <returns>The return value of the function delegate.</returns>
    public static TResult Using<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, TResult> func)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(func, nameof(func));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

#pragma warning disable 1998
        var dummyTask = disposable.UsingAsync(strategy, async (disposableInner) => func(disposableInner));
#pragma warning restore 1998

        return dummyTask.GetAwaiter().GetResult();
    }

    /// <summary>
    /// Executes the specified asynchronous delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="asyncFunc">The asynchronous delegate to execute using the disposable resource.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public static Task UsingAsync<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task> asyncFunc)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        return disposable.UsingAsync(strategy, async (disposableInner) =>
        {
            await asyncFunc(disposableInner);
            return true;   // dummy return value
        });
    }

    /// <summary>
    /// Executes the specified asynchronous function delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <typeparam name="TResult">The type of the return value of the asynchronous function delegate.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="asyncFunc">The asynchronous function delegate to execute using the disposable resource.</param>
    /// <returns>
    /// A task that represents the asynchronous operation. 
    /// The task result contains the return value of the asynchronous function delegate.
    /// </returns>
    public static async Task<TResult> UsingAsync<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task<TResult>> asyncFunc)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        Exception mainException = null;

        try
        {
            return await asyncFunc(disposable);
        }
        catch (Exception exception)
        {
            mainException = exception;
            throw;
        }
        finally
        {
            try
            {
                disposable.Dispose();
            }
            catch (Exception disposeException)
            {
                switch (strategy)
                {
                    case DisposeExceptionStrategy.Propagate:
                        throw;

                    case DisposeExceptionStrategy.Swallow:
                        break;   // swallow exception

                    case DisposeExceptionStrategy.Subjugate:
                        if (mainException == null)
                            throw;
                        break;    // otherwise swallow exception

                    case DisposeExceptionStrategy.AggregateMultiple:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw;

                    case DisposeExceptionStrategy.AggregateAlways:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw new AggregateException(disposeException);
                }
            }

            if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
                throw new AggregateException(mainException);
        }
    }
}
1 голос
/ 23 февраля 2009

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

0 голосов
/ 09 апреля 2013

Вот способ достаточно аккуратно захватить любые исключения, выданные содержимым using или Dispose.

Оригинальный код:

using (var foo = new DisposableFoo())
{
    codeInUsing();
}

Тогда вот код, который будет генерировать, если codeInUsing() throws или foo.Dispose() throws или оба throw, и позволит вам увидеть первое исключение (иногда в зависимости от InnerExeption):

var foo = new DisposableFoo();
Helpers.DoActionThenDisposePreservingActionException(
    () =>
    {
        codeInUsing();
    },
    foo);

Это не здорово, но не так уж плохо.

Вот код для реализации этого. Я установил его так, чтобы только работал, как описано, когда отладчик не подключен, потому что, когда отладчик подключен, меня больше беспокоит, что он сломается в нужном месте при первом исключении. Вы можете изменить при необходимости.

public static void DoActionThenDisposePreservingActionException(Action action, IDisposable disposable)
{
    bool exceptionThrown = true;
    Exception exceptionWhenNoDebuggerAttached = null;
    bool debuggerIsAttached = Debugger.IsAttached;
    ConditionalCatch(
        () =>
        {
            action();
            exceptionThrown = false;
        },
        (e) =>
        {
            exceptionWhenNoDebuggerAttached = e;
            throw new Exception("Catching exception from action(), see InnerException", exceptionWhenNoDebuggerAttached);
        },
        () =>
        {
            Exception disposeExceptionWhenExceptionAlreadyThrown = null;
            ConditionalCatch(
                () =>
                {
                    disposable.Dispose();
                },
                (e) =>
                {
                    disposeExceptionWhenExceptionAlreadyThrown = e;
                    throw new Exception("Caught exception in Dispose() while unwinding for exception from action(), see InnerException for action() exception",
                        exceptionWhenNoDebuggerAttached);
                },
                null,
                exceptionThrown && !debuggerIsAttached);
        },
        !debuggerIsAttached);
}

public static void ConditionalCatch(Action tryAction, Action<Exception> conditionalCatchAction, Action finallyAction, bool doCatch)
{
    if (!doCatch)
    {
        try
        {
            tryAction();
        }
        finally
        {
            if (finallyAction != null)
            {
                finallyAction();
            }
        }
    }
    else
    {
        try
        {
            tryAction();
        }
        catch (Exception e)
        {
            if (conditionalCatchAction != null)
            {
                conditionalCatchAction(e);
            }
        }
        finally
        {
            if (finallyAction != null)
            {
                finallyAction();
            }
        }
    }
}
...