.NET中的异常处理机制(二)

  本文我们继续通过另一个例子来讲解在C#中如何捕捉异常并进行处理。

  首先,我们新建一个控制台应用和一个Class Library Project。如下图所示。

.NET中的异常处理机制(二)_第1张图片

图1 ConsoleUI应用

.NET中的异常处理机制(二)_第2张图片

图2 ExceptionLibrary类库

  在ExceptionLibrary中,我们创建了一个Demo类,该类中有TopLayerMethod、MiddleLayerMethod和GetValue三个方法。这三个方法的实现是依次调用的关系。在控制台应用的Main方法中,我们新建了一个Demo实例并将引用赋给demo变量,然后在该变量上调用GetValue方法。我们知道GetValue方法中创建的numbers数组的长度为3,但Main方法中却访问索引为3的数组元素,这导致应用抛出一个异常。那么,让我们看看异常处理构造在何处定义的呢?观察代码可以发现是在ExceptionLibrary库的Demo类的GetValue方法中定义的。我们发现catch(Exception){}语句块,该语句块中并没有进行异常处理,而仅仅是捕捉到了异常,这消除了异常变成未处理异常(未处理异常异味着程序终止运行)的可能。注意图上红色圈圈部分的代码,若移除这些代码,编译器将报错,如下图所示。

.NET中的异常处理机制(二)_第3张图片

图3 编译器报并非所有路径都返回值错误

  这是因为当我们使用异常处理构造的时候,实际上应用程序在执行的时候存在两条可能的执行路径,一条是不发生异常的正常执行路径,一条是发生异常的执行路径,不论执行哪条路径都应该符合程序的语法语义,这里GetValue的返回值是int,故无论走哪条路径都要保证有int类型的返回值才行。

  我们上面的catch语句块存在的一个问题就是:我们吞噬了异常,好像异常根本没发生过一样!任何使用该应用的人都不会意识到已经发生了错误!应用带着错误的状态若无其事地继续欢快地运行!这怎么能行,我们至少应该让人知道发生了错误。对此我们做以下修正。

.NET中的异常处理机制(二)_第4张图片

图4 在catch语句块中捕获并处理异常

  .NET中的异常处理机制(二)_第5张图片

图5 控制台中打印出异常信息

  修正之后我们运行程序,可以看到控制台上打印出了异常信息,这提醒我们应用发生了错误,很好,至少我们知道了本该知道的事情!但是,这还不够,我们看控制台输出的最后一句,这表明程序在抛出异常之后带着错误的状态继续运行了,导致我们得到了错误的数据。为什么会这样呢?这是因为我们在catch语句块中仅仅打印了异常信息并未做其他任何处理。我们始终必须确保一件事:如果catch语句块中不包含throw语句,那么当执行该catch语句块下方的语句时,应用程序不应带着错误的状态数据继续运行。否则,大多数情况下我们希望应用程序立即终止运行,这样至少不会使问题进一步恶化。GetValue方法在这里实际上是不能决定应用程序是否该继续运行下去的!我们可以这样想象,GetValue方法只是最下层具体执行任务的小喽啰,当不妙的事情发生时,它是拿不定主意的。这时怎么办呢?好办,我们看它的上级(调用GetValue方法的方法)是谁,把裁决权还给它的上级就行了(即把异常继续向上抛给调用栈上方的方法,从而避免底层方法继续执行产生不希望的后果)。对此,我们做进一步修正,将GetValue方法中的try...catch异常构造注释掉,然后在ConsoleUI中添加try...catch,如下图所示。

.NET中的异常处理机制(二)_第6张图片

图6 将异常处理抛到ConsoleUI中处理

.NET中的异常处理机制(二)_第7张图片

图7 ConsoleUI中处理异常信息能显示完整的异常堆栈

  可以看出,由于异常处理权沿着调用栈一级一级向上传,直至ConsoleApplication3的Main方法中,从而阻断了GetValue方法的继续执行。这里也向我们揭示了异常处理中一个通用准则:尽可能地将在方法调用栈底层的异常向上抛,在上层处理,能尤其是在带UI界面并采用MVC模式(在Model和Controller层发生的异常需要抛给View层以给用户提供异常信息)开发的应用程序(比如WinForm或WPF应用)中更是如此,在这些带UI界面的应用中,只有UI层能够直接和用户交互,底层使用的类库是无法和用户交流的,因此,当我们需要将异常信息显示给用户时需要将异常从调用的底层类库向上抛给最上层的UI层。在异常向上层传递的过程中,我们还能够获得从异常抛出位置到异常捕捉位置经过的所有方法记录。

  也许你会有这样的疑问,即在异常向上传递的过程中,可以捕获并进行一些处理么?比如当异常传递到TopLayerMethod方法中时,可以捕捉然后进行一些处理吗?答案是:当然可以。也许存在这样一种情况,在TopLayerMethod方法中,我们首先打开了数据库连接,然后调用了MiddleLayerMethod方法,最后关闭数据库连接,但由于调用MiddleLayerMethod时发生了异常,导致数据库连接不能关闭。

.NET中的异常处理机制(二)_第8张图片

图8 异常导致TopLayerMethod方法中的数据库连接不能关闭

  这时怎么办呢?解决办法有两个。

  1. 使用try...finally块,将资源清理的工作放在finally块中。
  2. 使用try...catch...finally块。在catch块中捕捉异常并将异常信息记录到日志文件(有一种说法,记录日志文件不算异常处理,真正的异常处理必须向用户明确提示应用出现了错误,即在UI层显示异常信息),然后重新抛往上层。注意,若在catch块中除了throw语句外没有做其他处理,那么可省略该catch,也即直接使用1中的try...finally块。

.NET中的异常处理机制(二)_第9张图片

图9 使用try...catch或try...catch...finally关闭数据库连接

  下面让我们看几个初学者可能犯的错误。

  1. 将上图中的catch块中的throw改为throw ex,这将导致CLR重置异常抛出的起点,不利于发现异常真实发生的位置。不建议使用。
  2. 将throw改为throw new Exception("程序出现错误")。相对于1来说,它向用户提供了更加友好的自定义信息,但是它存在和1同样的问题。不建议使用。

  此外,在开发中,我们也可能遇到这种情况,我们在catch语句中抛出了另外一个新异常,比如ArgumentException,那么我们怎么在抛出这个新异常时不丢失之前的异常信息呢?这就需要借助于inner exception了,我们可以这样使用throw new ArgumentException("输入参数有误",ex),这样就可以将之前抛出的异常作为新异常的一个属性,之后在UI层的catch语句中就可以使用两个异常的信息了。需要注意的是,在UI层处理内部异常时,可能存在异常的嵌套。为了获取所有的异常信息,我们可以使用while循环遍历内部异常,直到内部异常为空为止。若仅需获取根异常,忽略中间层异常,那么可以使用GetBaseException方法。另外,有网友反映GetBaseException有时不能获取到最原始的异常(root exception),为了解决这一问题,可以自定义一个获取root exception的方法,该方法内部采用递归调用实现。

public static class ExceptionExtensions
{
    public static Exception GetOriginalException(this Exception ex)
    {
        if (ex.InnerException == null) return ex;
        return ex.InnerException.GetOriginalException();
    }
}

.NET中的异常处理机制(二)_第10张图片

图10 遍历所有嵌套内部异常并打印异常堆栈信息

  最后,小结一下:在实际开发中,我们常使用类似MVC的模式开发带UI的应用,我们在Model层和Controler层发生的异常一定不能吞噬掉,应尽可能向上抛,在向上抛的过程中可以捕获该异常并做一些处理(比如将异常信息写入日志文件),或者不捕获,而是用finally块做一些资源清理工作(比如关闭打开的数据库连接),但最终还是需要抛向UI层以便及早发现程序的问题。

转载于:https://www.cnblogs.com/lian--ying/p/9248118.html

你可能感兴趣的:(.NET中的异常处理机制(二))