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

时间:2009-02-23 13:24:38

标签: c# .net idisposable

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

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

  1. 抛出第一个异常
  2. 由于第一个异常而执行finally块
  3. finally块调用Dispose()方法
  4. Dispose()方法抛出第二个异常
  5. ...您的流程不会立即终止。但是,由于.NET无法用第二个异常替换第一个异常,因此会丢失信息。因此,调用堆栈上某处的catch块将永远不会出现第一个异常。然而,人们通常对第一个例外更感兴趣,因为这通常会为事情开始出错提供更好的线索。

    由于.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's assume that this might throw
            }
            catch
            {
            }
        }
    }
    

8 个答案:

答案 0 :(得分:36)

Framework Design Guidelines(2 nd ed)将此作为(§9.4.1):

  

避免从Dispose(bool)中抛出异常,但在严重情况下除外   包含进程已损坏的情况(泄漏,不一致)   共享国家等。)。

评论[编辑]:

  • 有指导方针,而不是硬性规则。这是一个“避免”而不是“不要”的指导方针。如上所述(在评论中)框架打破了这个(和其他)指导方针。诀窍是知道何时打破指南。在很多方面,这是一个熟练工和大师之间的区别。
  • 如果清理的某些部分可能失败,那么应该提供一个Close方法,该方法将抛出异常,以便调用者可以处理它们。
  • 如果您正在遵循dispose模式(如果类型直接包含某些非托管资源,则应该是这样),那么可以从终结器中调用Dispose(bool),从终结器中抛出是一个坏主意并将阻止其他对象来自最终确定。

我的视图:从Dispose中逃逸的异常应该只是那些,如指南中那样,足以造成灾难性的,以至于当前流程无法提供更可靠的功能。

答案 1 :(得分:17)

我认为吞咽是这种情况下两种邪恶中较小的一种,因为最好提高原始 Exception - 警告:除非 ,也许干净地处理的失败本身就非常严重(可能是TransactionScope无法处置,因为这可能表示回滚失败)。

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

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

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

答案 2 :(得分:6)

应该设计

Dispose来实现其目的,处理对象。此任务是安全的,并且在大多数情况下不会抛出异常。如果你发现自己从Dispose抛出异常,你可能应该三思而后行,看看你是否做了太多的东西。除此之外,我认为Dispose应该像所有其他方法一样对待:处理如果你可以用它做某事,如果你做不到就让它冒泡。

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

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

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

答案 3 :(得分:2)

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

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

答案 4 :(得分:2)

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

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

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

答案 5 :(得分:1)

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

答案 6 :(得分: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

更新:如果您需要支持返回值和/或异步的委托,那么您可以使用这些重载:

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

答案 7 :(得分:0)

这是一种方法,可以相当干净地抓取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();
            }
        }
    }
}