Если вы реализуете IDisposable.Dispose(), чтобы он никогда не выбрасывал?

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

В эквивалентном сценарии в .NET...

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

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

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

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

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

РЕДАКТИРОВАТЬ. Чтобы уточнить, я не говорю о том, чтобы активно отбрасывать исключения из 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 assume that this might throw
        }
        catch
        {
        }
    }
}

Ответ 1

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

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

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

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

Ответ 2

Руководство по разработке каркаса (2 nd ed) имеет это как (§9.4.1):

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

Комментарий [Изменить]:

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

Слишком плохо Microsoft не предоставила параметр Exception для Dispose с намерением, чтобы он был обернут как исключение InnerException, если сама утилита выдает исключение. Разумеется, для эффективного использования такого параметра потребуется использовать блок исключения-фильтра, который С# не поддерживает, но, возможно, существование такого параметра могло бы побудить дизайнеров С# обеспечить такую ​​функцию? Одним из приятных вариантов, которые я хотел бы увидеть, было бы добавление параметра "Исключение" в блок "Наконец", например

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

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

Ответ 6

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

Ответ 7

Существуют различные стратегии распространения или проглатывания исключений из метода 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 Qaru question
    /// <see href="/questions/22847/swallowing-exception-thrown-in-catchfinally-block">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);
        }
    }
}

Ответ 8

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

Исходный код:

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

Затем вот код, который будет бросать, если либо codeInUsing() бросает, либо foo.Dispose() бросает или бросает, и позволяет увидеть первое исключение (иногда завернутое как 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();
            }
        }
    }
}