65.9K
CodeProject 正在变化。 阅读更多。
Home

在(抛出)异常后继续执行代码

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2017年12月21日

CPOL

7分钟阅读

viewsIcon

48481

downloadIcon

326

一种轻松切换异常处理方式(是抛出异常还是由自定义代码处理)的方法,同时在不抛出异常时保留堆栈跟踪。

本解决方案旨在作为一个起点。由于其实现简单,可以轻松地进行修改以满足您的需求。

引言

本文提供了一种方法(及实现),可以轻松切换异常的处理方式(是抛出异常还是由自定义代码处理),同时在不抛出异常时保留堆栈跟踪。

背景

我个人喜欢在编写程序时非常严格。在编写一个方法时,如果我认为某个执行路径没有意义,或者不应该发生,我就会抛出异常。每个线程只有一个用于处理异常的 catch 方法。

这样,潜在的 bug 就能尽早被捕获,并且由于异常中的数据(异常类型、消息和堆栈跟踪)而易于解决。

这听起来不错!但让我们进入现实世界……

想象一下,有几个开发人员使用上述方法处理一个产品。产品开始变得越来越大,包含多个组件。有一天,您或您的同事有点粗心,没有经过充分测试就提交了代码。没问题:一个异常弹出来了!然而,这个异常现在弹给所有参与该产品开发的人。即使是正在开发完全不同组件的开发人员,也会被阻止,直到这个异常被解决。

当然,这是一个非常粗糙的例子,异常处理可以按组件或类进行。但根据我的经验,根据使用者的不同,切换异常处理方式可能会非常有用。

本文就是关于这个的:切换异常是应该抛出,还是以其他方式处理。

那么……为什么不直接记录日志呢?

当我抛出异常时,是因为我无法保证程序在发生某个执行路径时的稳定性。

然而,在团队希望避免应用程序因单个异常而崩溃的项目中(参见上一章的示例),这些异常可能会在多个位置(尽早)被捕获并记录下来,或者这类异常被日志记录完全取代。

用日志记录替换异常不一定是一件坏事。但在某些情况下,应用程序可能不会显示任何症状或问题,当这种“异常”发生时。如果这种情况经常发生,日志文件最终会填满异常,使得找到真正的问题越来越困难。

最重要的是,被记录/忽略的“异常”可能会随着时间的推移导致程序不稳定。如果日志中充斥着大量平凡的异常,将很难找到由此导致的 bug 的原始原因。

但是……异常(和堆栈跟踪)不是很慢吗?

抛出异常和创建调用堆栈可能会相对较慢。但这就是为什么它们应该只在……异常情况下使用。我提供的这个方法并不意味着您应该完全关闭异常的抛出,并忽略代码中发生的一切,只要它仍然看起来正常

本文/解决方案的目的是让开发人员能够(主要是在开发过程中)在抛出异常和以其他方式处理它们之间进行切换。发布的代码中发生的每一个异常仍然是……

使用代码

抛出/创建异常

代替抛出异常,异常将通过一个名为 ExceptionHandler 的类进行处理。该类决定是抛出异常还是由自定义代码处理。

为了避免在每个地方都去检索 ExceptionHandler 对象,包含的库使用了 Exception 类的扩展方法。当然,您可以自由地按照自己认为合适的方式来实现这一部分。

private void Example()
{
    throw new NullReferenceException("This is an example");

    // Some logic
}

将被替换为

private void Example()
{
    new NullReferenceException("This is an example").Handle();
    return;

    // Some logic
}

当方法有返回值时,由开发人员决定在异常情况下方法应该返回什么。为了简化,Handle<TReturn> 方法允许开发人员提供一个返回值。

private int Example()
{
    throw new NullReferenceException("This is an example");

    // Some logic
}

可能被替换为

private const int ErrorValue = -1;

private int Example()
{
    new NullReferenceException("This is an example").Handle();
    return ErrorValue;

    // Some logic
}
或通过
private const int ErrorValue = -1;

private int Example()
{
    return new NullReferenceException("This is an example").Handle(ErrorValue);

    // Some logic
}

处理异常

当使用上述异常时,开发人员希望如何处理它们可以在 ExceptionHandler 类中进行操作。

ExceptionHandler 是一个基本观察者模式的主题,允许类型为 IUnthrownExceptionHandler 的观察者向其注册。如果 ThrowExceptions 属性设置为 false,则一旦异常被传递给 ExceptionHandler,所有注册处理程序的 Handle 方法都会被调用。

public interface IUnthrownExceptionHandler
{
    void Handle(UnthrownException unthrownException);
}

注意: Handle 方法接受 UnthrownException 的实例,而不是 Exception 的实例。这在下一章中解释。

一个简单的将异常写入控制台输出的例子

首先创建一个实现 IUnthrownExceptionHandler 的类。

class ConsoleExceptionHandler : IUnthrownExceptionHandler
{
    public void Handle(UnthrownException unthrownException)
    {
        Console.WriteLine(unthrownException);
    }
}

然后将其注册到 ExceptionHandler

ExceptionHandler.Instance.ThrowExceptions = false;
ExceptionHandler.Instance.RegisterHandler(new ConsoleExceptionHandler());

现在,当异常由 ExceptionHandler 处理时,它将被写入控制台输出。这个例子可以在附件的解决方案中找到。

注意:ThrowExceptionstrue 时,异常将由 ExceptionHandler 直接抛出。注意: 异常的堆栈跟踪也将包含 ExceptionHandlerHandle 方法。

未抛出的异常

当异常不被 ExceptionHandler 抛出时,会为其创建一个 UnthrownExceptionUnthrownException 类具有以下属性:

  • Exception - 原始异常类。
  • StackTrace - 异常的堆栈跟踪,以字符串形式。

注意: 捕获异常时,堆栈跟踪会被缩短,仅包含从捕获位置到异常抛出位置的跟踪。由于异常在堆栈中被处理,StackTrace 将包含直到调用 Handle 方法的完整堆栈。

  • Origin - ExceptionOrigin 类的实例,包含异常来源的信息。

ExceptionOrigin 的属性

  • CalledType - 创建异常的类的类型。
  • Method - 创建异常的方法名称。
  • Line - 创建异常的文件的行号。
  • Column - 创建异常的文件的列号。

使用 ExceptionOrigin 的一个例子是过滤某些异常,或者用它来对重复发生的异常进行分组。

我应该如何处理异常?

这取决于您。我创建的解决方案只是一个工具,让它成为可能。

但由于异常可能表明一个严重的问题,它们不应该被忽视。您可能想将它们记录到专门的异常日志中。对于已发布的版本,在发生异常时发送电子邮件给开发人员也可能有用。

何时处理,何时抛出?

同样,这取决于您。但一些例子可能是

抛出

  • 在开发机器上 - 不要让开发人员逃避异常!对于上面例子的情况,只需暂时禁用它们,将 ThrowExceptions 设置为 false。
  • 在测试机器上 - 在测试期间,找到任何异常都非常重要。

处理

  • 已发布的版本 - 这当然因情况而异,但一个例子可能是编写处理程序,在关键组件中抛出异常,但只记录不太重要组件的异常。
  • 演示 - 在进行演示时禁用异常可能很有用。对于一些利益相关者来说,屏幕上消失一个东西可能比错误弹窗更好。

关注点

  • 解决方案旨在作为一个起点。由于其实现简单,可以轻松地进行修改以满足您的需求。
  • 默认情况下,ExceptionHandler 使用单例模式。当然,您可以忽略它。
  • 如果异常未被抛出,finally 块将在异常处理后执行。我曾考虑向 Handle 方法添加一个可选的 Action,但我迄今为止还没有这个需求。
  • 我选择将 Exception 类传递给 Handle 方法(而不是只在那里写一条消息,并在需要时创建一个异常)有三个原因:
    • 我想尽量减少对用户端的影响。现在,只需要将 throw 替换为 Handle 调用。
    • 仍然可以清楚地看到某个执行路径是异常情况。
    • Exception 类的类型本身也包含关于异常的信息。当处理 NullReferenceException 时,您就知道该查找什么。这也使得查找特定异常的用法更加容易。
  • 我对 ExceptionHandler 这个名字不太满意,因为它可能与 IUnthrownExceptionHandler 结合使用时造成混淆。我乐于接受建议。

历史

2017-12-21 - 版本 1
2018-01-02 - 版本 1.1
  • ExceptionExt 中重新抛出异常,以最小化由 ExceptionHandling 库创建的堆栈跟踪部分。
2018-01-03 - 版本 1.2
  • 用户无法在 Handle 方法中指定返回值。
  • ExceptionExt 中的 Handle 方法将不再重新抛出异常,因为它可能会隐藏由 IUnthrownExceptionHandler 的自定义实现抛出的异常。相反,如果需要,异常现在会立即抛出,同时也最小化了堆栈跟踪,因为它不会包含 ExceptionHandling 类。
© . All rights reserved.