首页 文章

你应该实现IDisposable.Dispose(),以便它永远不会抛出?

提问于
浏览
52

对于C(析构函数)中的等效机制,建议是it should usually not throw any exceptions . 这主要是因为这样做可能会终止您的流程,这很少是一个好策略 .

在.NET的等效场景中......

  • 抛出第一个异常

  • 由于第一个异常,执行finally块

  • finally块调用Dispose()方法

  • Dispose()方法抛出第二个异常

...您的流程不会立即终止 . 但是,由于.NET无法用第二个异常替换第一个异常,因此会丢失信息 . 因此,调用堆栈上某处的catch块将永远不会出现第一个异常 . 然而,人们通常对第一个例外更感兴趣,因为这通常会提供更好的线索,说明为什么事情开始出错 .

由于.NET缺少一种机制来检测代码是否在异常处于挂起状态时被执行,因此似乎只有两种选择可以实现IDisposable:

  • 始终吞下Dispose()中发生的所有异常 . 不好,因为你可能最终吞下OutOfMemoryException,ExecutionEngineException等等,我通常宁愿在它们发生时拆除它而没有另外的异常已经挂起 .

  • 让所有异常传播出Dispose() . 不好,因为您可能会丢失有关问题根本原因的信息,请参阅上文 .

那么,两个邪恶中哪一个较小?有没有更好的办法?

EDIT :为了澄清,我在讨论让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 回答

  • 1

    我认为吞咽是这种情况下两种邪恶中较小的一种,因为提高原始状态更好 - 警告: unless ,也许未能干净地处置它本身就非常关键(也许如果 TransactionScope 无法处理,因为这可能表示回滚失败) .

    有关此问题的更多想法,请参阅here - 包括包装/扩展方法的想法:

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

    当然,你也可以做一些奇怪的事情,你用原始和第二个( Dispose() )异常重新抛出一个复合异常 - 但是想一想:你可能有多个 using 块......它很快就会变得无法管理 . 实际上,最初的例外是有趣的 .

  • 17

    Framework Design Guidelines(第2版)将此作为(§9.4.1):

    避免在Dispose(bool)中抛出异常,除非在包含进程已损坏的严重情况下(泄漏,不一致的共享状态等) .

    评论[编辑]:

    • 有指导方针,而不是硬性规则 . 这是一个"AVOID"而不是"DO NOT"指南 . 如上所述(在评论中)框架打破了这个(和其他)指导方针 . 诀窍是知道何时打破指南 . 在许多方面,这是一个熟练工和大师之间的区别 .

    • 如果清理的某些部分可能失败,那么应该提供一个Close方法,该方法将抛出异常,以便调用者可以处理它们 .

    • 如果您正在遵循dispose模式(如果类型直接包含某些非托管资源,则应该是这样),那么可以从终结器中调用 Dispose(bool) ,从终结器中抛出是一个坏主意,并且会阻止其他对象被最终确定 .

    我的观点:从Dispose中逃避的异常应该只是那些,如在指南中那样,充分的灾难性,以至于当前的过程不可能有进一步的可靠功能 .

  • 6

    Dispose 应该设计用于实现其目的,处理对象 . 这个任务 is safe and does not throw exceptions most of the time . 如果你发现自己从 Dispose 抛出异常,你可能应该三思而后行,看看你是否做了太多的东西 . 除此之外,我认为 Dispose 应该像所有其他方法一样对待:处理如果你可以用它做某事,如果你做不到就让它冒泡 .

    编辑:对于指定的示例,我会编写代码,以便我的代码不会导致异常,但清除 TcpClient 可能会导致异常,这应该在我看来有效传播(或处理和重新抛出更多一般异常,就像任何方法一样):

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

    但是,就像任何方法一样,如果你知道 tcpClient.Close() 可能抛出一个应该被忽略的异常(无关紧要)或者应该被另一个异常对象表示,那么你可能想 grab 它 .

  • 2

    释放资源应该是一种“安全”操作 - 毕竟如何从无法释放资源中恢复?所以从Dispose抛出异常是没有意义的 .

    但是,如果我在Dispose中发现程序状态已损坏,最好抛出异常然后吞下它,最好现在粉碎然后继续运行并产生不正确的结果 .

  • 2

    太糟糕了,微软没有为Dispose提供Exception参数,意图将它包装为InnerException,以防处理本身抛出异常 . 可以肯定的是,有效使用这样的参数需要使用C#不支持的异常过滤器块,但是这样的参数的存在是否可能促使C#设计者提供这样的功能?我希望看到的一个不错的变化是向Finally块添加Exception“参数”,例如

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

    这将表现得像一个普通的最后一个块,除了'ex'将为null / Nothing如果'Try'运行完成,或者如果没有则保持抛出的异常 . 太糟糕了,没有办法让现有的代码使用这样的功能 .

  • 0

    我可能会使用日志记录来捕获有关第一个异常的详细信息,然后允许引发第二个异常 .

  • 1

    有各种策略可以从 Dispose 方法传播或吞咽异常,可能是基于是否还从主逻辑抛出了无法处理的异常 . 最佳解决方案是将决策权交给调用者,具体取决于他们的具体要求 . 我已经实现了这样做的通用扩展方法,提供:

    这是我的扩展方法:

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

    Update :如果需要支持返回值和/或异步的委托,则可以使用这些重载:

    /// <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);
            }
        }
    }
    
  • 35

    这是一种相当干净地抓取 usingDispose 内容抛出的任何异常的方法 .

    原始代码:

    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();
                }
            }
        }
    }
    

相关问题