56

对于 C++ 中的等效机制(析构函数),建议它通常不应该抛出任何异常。这主要是因为这样做你可能会终止你的进程,这很少是一个好的策略。

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

  1. 抛出第一个异常
  2. 由于第一个异常而执行 finally 块
  3. finally 块调用 Dispose() 方法
  4. Dispose() 方法引发第二个异常

...您的进程不会立即终止。但是,您会丢失信息,因为 .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
        {
        }
    }
}
4

8 回答 8

36

框架设计指南(第2版)的内容如下(第 9.4.1 节)

避免在 Dispose(bool) 中引发异常,除非在包含进程已损坏(泄漏、不一致的共享状态等)的危急情况下。

评论[编辑]:

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

我的观点:从 Dispose 逃逸的异常应该只是那些,如指南中那样,具有足够的灾难性,以至于当前进程无法提供进一步的可靠功能。

于 2009-02-23T13:41:36.500 回答
16

我会争辩说,在这种情况下,吞咽是两个弊端中较小的一个,因为最好提出原来 Exception的警告:除非,也许不能干净地处理它本身就是非常关键的(也许如果一个TransactionScope不能处理,因为这可能表示回滚失败)。

有关这方面的更多想法,请参见此处- 包括包装器/扩展方法的想法:

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

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

于 2009-02-23T13:29:27.667 回答
6

Dispose应该设计做它的目的,处置对象。此任务是安全的,并且大部分时间不会引发异常。如果您看到自己从 中抛出异常Dispose,您可能应该三思而后行,看看您是否在其中做了太多事情。除此之外,我认为Dispose应该像对待所有其他方法一样对待:如果你能用它做某事,就处理它,如果你不能,让它冒泡。

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

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

但是,就像任何方法一样,如果您知道tcpClient.Close()可能会抛出一个应该被忽略(没关系)或应该由另一个异常对象表示的异常,您可能想要捕获它。

于 2009-02-23T13:31:17.897 回答
3

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

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

于 2009-02-23T13:53:10.950 回答
2

微软没有为 Dispose 提供 Exception 参数太糟糕了,目的是将其包装为 InnerException 以防处理本身引发异常。可以肯定的是,有效使用此类参数需要使用 C# 不支持的异常过滤器块,但也许此类参数的存在可能促使 C# 设计人员提供此类功能?我希望看到的一个不错的变化是将异常“参数”添加到 finally 块,例如

  最后异常 ex: // 在 C# 中
  最后 Ex as Exception ' 在 VB

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

于 2011-01-16T23:02:38.247 回答
2

从方法中传播或吞下异常有多种策略Dispose,可能基于是否也从主逻辑中抛出了未经处理的异常。最好的解决方案是将决定权留给调用者,具体取决于他们的具体要求。我已经实现了一个通用的扩展方法,它提供:

  • using传播Dispose异常的默认语义
  • Marc Gravell 关于总是吞下Dispose异常的建议
  • maxyfcDispose仅在主逻辑中存在异常时才吞下异常的替代方案,否则该异常将丢失
  • Daniel Chambers将多个异常包装到一个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);
        }
    }
}
于 2016-02-24T21:41:36.360 回答
1

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

于 2009-02-23T13:40:07.627 回答
0

这是一种相当干净地抓取由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();
            }
        }
    }
}
于 2013-04-09T16:17:44.867 回答