Вы должны реализовать интерфейс IDisposable.Метод Dispose (), так что он никогда не бросает?


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

в эквиваленте сценарий .Чистая ...

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

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

поскольку .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 52

8 ответов:

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

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

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

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

на Основные Принципы Проектирования (2 nd ed) имеет это как (§9.4.1):

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

Комментарий [Edit]:

  • есть рекомендации, а не жесткие правила. И это "избежать", а не " не делать" директива. Как отмечалось (в комментариях), структура местами нарушает это (и другие) руководящие принципы. Хитрость в том, чтобы знать, когда нарушить руководство. В этом, во многом, и заключается разница между подмастерьем и мастером.
  • если какая-то часть очистки может потерпеть неудачу, то следует предоставить метод Close, который будет выдавать исключения, чтобы вызывающий мог их обрабатывать.
  • если вы следуете шаблону dispose (и вы должны быть, если тип непосредственно содержит некоторый неуправляемый ресурс) тогда Dispose(bool) может быть вызван из финализатора, бросание из финализатора-плохая идея и заблокирует другие объекты от завершения.

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

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

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

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

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

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

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

жаль, что Microsoft не предоставила параметр исключения для Dispose, с намерением, чтобы он был обернут как InnerException в случае, если disposal сам создает исключение. Конечно, эффективное использование такого параметра потребует использования блока фильтра исключений, который C# не поддерживает, но, возможно, существование такого параметра могло бы побудить дизайнеров C# предоставить такую функцию? Одним из приятных вариантов, которые я хотел бы видеть, было бы добавление исключения "параметр" к блоку Finally, например

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

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

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

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

  • по умолчанию using семантика пропаганды Dispose исключения
  • Марк Gravell-это всегда глотании 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);
        }
    }
}

вот способ довольно чисто захватить любые исключения, вызванные содержимым 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();
            }
        }
    }
}