你应该实现IDisposable.Dispose()方法,使它永远不会抛出异常吗?

57
对于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
        {
        }
    }
}

1
对于背景:WCF以此闻名… - Marc Gravell
8个回答

38

框架设计准则(第二版)中第9.4.1节规定:

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

评论 [编辑]:

  • 这些是指南,而不是硬性规定。这是一个"避免"而不是"不要"的指南。正如注释中所述,框架在某些地方打破了这个(和其他)指南。诀窍在于知道何时打破指南。在很多方面,这就是学徒和大师之间的区别。
  • 如果清理的某个部分可能失败,则应提供一个Close方法,以便调用者可以处理它们。
  • 如果你正在遵循dispose模式(如果该类型直接包含一些非托管资源,则应该这样做),那么Dispose(bool)可能会从finaliser中调用,从finaliser中抛出异常是一个坏主意,并且会阻止其他对象被finalised。

我的观点:从Dispose中逃逸的异常应该只有那些像指南中所述的那样灾难性,以至于当前进程不可能再有可靠的功能。


1
默认情况下,不要让任何东西逃脱。如果在 finaliser 中包含的操作有很大可能失败(例如关闭网络资源),则提供单独的 Close 方法。 - Richard
我喜欢将一个会抛出异常的Close方法和不会抛出异常的Dispose方法结合在一起的想法。如果可以的话,加2分。 - Ignacio Soler Garcia
这里的大问题在于AVOID实际上是没有意义的,因为事先无法知道Dispose是否会抛出异常,因此原则上必须假设它会。各种与网络相关的.NET类在处理时可能会抛出异常。 - nicodemus13
@nicodemus13 AVOID并不是毫无意义的,大多数类型都遵循这个规则。如果您有不遵循此规则的类型(提供具体示例会更有帮助),请检查是否存在Close方法,该方法可以用于以更可控的方式执行清理工作。 - Richard
@MartinBa 当遵循该模式时,Dispose 只是调用 Dispose(bool)它确实适用 - Richard
显示剩余5条评论

16

在这种情况下,我认为吞咽异常是两种罪恶中较小的一种,因为最好引发原始异常 - 警告:除非不能干净地处理自己本身是相当关键的失败(比如一个 TransactionScope 无法释放,因为那可能表示回滚失败)。

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

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

当然,你也可以做一些奇怪的事情,在抛出一个包含原始异常和第二个(Dispose())异常的组合异常 - 但是想想:你可能有多个using块...它很快就变得不可控了。实际上,原始异常才是最有意义的。


2
我同意它可能变得难以管理,但一个重要的观点是,除非没有原始异常,否则原始异常才是有趣的。我继续写下了这个奇怪的问题,并且对它感到非常满意,尽管在我的特定情况下,我最终只是在Dispose内部吞噬了异常(因为我可以修改代码并且不想让每个类的用户都去访问)。 - aggieNick02
这是正确的,并且突显了TransactionScope设计中的一个弱点。另一方面,即使出现电源故障,事务也不能失败回滚,因为这是默认操作。TransactionScope应该在Dispose期间忽略异常。然而,实际上,Dispose执行提交工作,而Commit()仅标记要提交但实际上并未提交。也许是另一个设计错误。 - usr

8

Dispose 应当被设计用于执行它的目的:释放对象。这个任务通常是安全的,并且在大多数情况下不会抛出异常。如果你发现自己从 Dispose 中抛出异常,你应该再三思考一下是否在其中进行了过多的操作。除此之外,我认为 Dispose 应该像其他所有方法一样被处理:如果可以处理就进行处理,如果不能则让其冒泡。

编辑:对于给定的示例,我会编写代码,以便 我的代码 不会导致异常,但清理 TcpClient 可能会引发异常,在我看来,这种异常应该被传播(或者将其处理并重新抛出为更通用的异常,就像任何方法一样):

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

然而,就像任何方法一样,如果您知道 tcpClient.Close() 可能会抛出一个应该被忽略(无关紧要)或应该由另一个异常对象表示的异常,那么您可能需要捕获它。


冒泡会丢失最初破坏的实际内容。关于这个模式,最好告诉WCF团队 ;-p - Marc Gravell
我澄清了问题。显然,调用TcpClient.Close()并不算做太多的操作,对吧? - user49572

4
释放资源应该是一个“安全”的操作——毕竟,如果无法释放资源,我如何恢复呢?所以从Dispose中抛出异常就没有意义。
然而,如果我在Dispose内发现程序状态已经损坏,抛出异常比将其忽略更好,最好现在崩溃,而不是继续运行并产生错误的结果。

如果尝试Dispose TCP连接成功,代码可以假设已发送的所有内容都已到达另一端。如果尝试失败,则代码不应在假定信息已传递的情况下继续进行,但这并不意味着整个系统状态已损坏。 - supercat
@supercat - 我写道,释放资源应该是一种安全操作,除非系统状态已经损坏(而且,根据被接受的答案,框架设计指南也同意我的看法)。我从未说过 .net 中的所有类都遵循这个准则(有些不遵循)。我个人认为,你描述的 TCP 连接行为是错误的(因为“信息已经传递”并不意味着什么,如果服务器在发送 TCP ACK 后立即崩溃,数据就和没有发送一样丢失),而且我在 TcpClient MSDN 文档中找不到这种行为。 - Nir
TCP并不是一个完美的例子,但它对许多人来说很熟悉;也许更好的例子是尝试将文件保存到可移动驱动器中,这可能没有任何其他用途。如果有人在文件写入之前拔出USB存储设备,任何结果异常都不应该被压制,但是拔出存储设备不应该干扰未使用它的应用程序的部分[除其他事项外,由于用户的文档未保存,用户可能会想再次尝试保存它!] - supercat
@supercat - 如果拔掉USB驱动器导致Dispose出现错误,因为Dispose试图将先前写入的数据刷新到驱动器中,那么系统状态就变得有点不正常了(你之前执行的那些Write调用返回成功,但实际上它们失败了,你在假设它们成功的情况下所做的任何操作可能都是无效的)。如果拔掉驱动器导致Dispose出现错误,因为Dispose执行文件操作而不应该这样做,Dispose是用于清理和释放资源(有时还会中止)而不是用于保存东西。 - Nir
@supercat...即使你没有收到异常,也不能基于文件实际已保存的假设是正确的,因为由于操作系统和硬件缓存,除非你使用FlushFileBuffers或写入方式,否则你无法确定数据是否已写入。FlushFileBuffers是一个影响每个进程性能的系统级操作,不是你想在Dispose时自动执行的操作,如果你使用写入方式,那么你会过于关注数据何时被写入以使用自动刷新。 - Nir
显示剩余2条评论

3
很遗憾微软没有为Dispose方法提供一个Exception参数,这个参数本意是在处理过程中出现异常时将其作为内部异常InnerException包装。当然,要使用此参数,需要使用异常过滤器块,但C#不支持它,也许这样一个参数的存在可以激励C#设计者提供这样的功能?一个不错的改进是我希望看到Finally块添加一个异常“参数”,例如:

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

它会像一个普通的Finally块一样运行,但如果“Try”成功完成,“ex”将为null / Nothing,否则将保存抛出的异常。“太遗憾没有办法让现有代码使用这样的功能。


2
有多种策略可以从Dispose方法中传播或吞咽异常,可能基于主逻辑是否也抛出了未处理的异常。最好的解决方案是根据调用者的具体要求让决定权归他们所有。我已经实现了一个通用的扩展方法,它提供以下功能:
  • 默认的using语义传播Dispose异常
  • Marc Gravell的建议总是吞咽Dispose异常
  • maxyfc的替代方案只在主逻辑中会丢失的情况下吞咽Dispose异常
  • 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://dev59.com/3nI-5IYBdhLWcg3w0cKG">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);
        }
    }
}

我非常喜欢这个,但是可以使用异常过滤器在C# 6中进行改进。在外部try块中,您可以使用catch (Exception exception) when (null == (mainException = exception)) {}来捕获主要异常,catch (Exception exception) when (strategy == DisposeExceptionStrategy.AggregateAlways)来聚合主要异常,在内部try块中使用catch when (strategy == DisposeExceptionStrategy.Swallow || (strategy == DisposeExceptionStrategy.Subject && mainException != null)) {}等。然后,所有的堆栈跟踪都将包括对action和/或Dispose的实际调用,而不是重新抛出的异常。 - MattW

1
我会使用日志记录第一个异常的详细信息,然后允许第二个异常被抛出。

0

这里有一种相当干净的方法来捕获由usingDispose中的内容抛出的任何异常。

原始代码:

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

下面是一段代码,如果codeInUsing()foo.Dispose()抛出异常,或者两者都抛出异常,它将会抛出异常,并让你看到第一个异常(有时作为InnerException包装):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接