7.2 错误处理
本章的第一部分讨论如何在应用程序的开发过程中查找和改正错误,使这些错误不会在发布的代码中出现。但有时,我们知道可能会有错误发生,但不能100%地肯定它们不会发生。此时,最好能预料到错误的发生,编写足够健壮的代码以处理这些错误,而不必中断程序的执行。
错误处理就是用于这个目的。本节将介绍异常和处理它们的方式。异常是在运行期间代码中产生的错误,或者由代码调用的函数产生的错误。这里的“错误”定义要比以前更含糊,因为异常可能是在函数等结构中手工产生的。例如,如果函数的一个字符串参数不是以a开头,就产生一个异常。严格来讲,从该函数的外部看这并不是一个错误,但调用该函数的代码会把它看成错误。
在本书前面已经遇到几次异常了。最简单的示例是试图定位一个超出范围的数组元素,例如:
int[] myArray = { 1, 2, 3, 4 }; int myElem = myArray[4];
这会产生如下异常信息,并中断应用程序的执行:
Index was outside the bounds of the array.
异常在名称空间中定义,大多数异常的名称清晰地说明了它们的用途。在这个示例中,产生的异常称为System.IndexOutOfRangeException,说明我们提供的myArray数组索引不在允许使用的索引范围内。只有在异常未处理时,这个信息才会显示出来,应用程序也才会中断执行。下一节将讨论如何处理异常。
7.2.1 try...catch...finally
C#语言包含结构化异常处理(Structured Exception Handling, SEH)的语法。用3个关键字可以标记出能处理异常的代码和指令,如果发生异常,就使用这些指令处理异常。用于这个目的的3个关键字是try、catch和finally。它们都有一个关联的代码块,必须在连续的代码行中使用。其基本结构如下:
try
{
...
}
catch (<exceptionType> e) when (filterIsTrue)
{
<await methodName(e); >
...
}
finally
{
<await method name>
...
}
也可以在catch或finally块内使用C# 6引入的await。await关键字用于支持先进的异步编程技术,避免瓶颈,且可以提高应用程序的总体性能和响应能力。利用async和await关键字的异步编程在本书中不讨论;然而,这些关键字简化了这个编程技术的实现,所以强烈建议学习它们。
也可以只有try块和finally块,而没有catch块,或者有一个try块和好几个catch块。如果有一个或多个catch块,finally块就是可选的,否则就是必需的。这些代码块的用法如下:
● try—— 包含抛出异常的代码(在谈到异常时,C#语言用“抛出”这个术语表示“生成”或“导致”)。
● catch——包含抛出异常时要执行的代码。catch块可以使用<exceptionType>,设置为只响应特定的异常类型(如System.IndexOutOfRangeException),以便提供多个catch块。还可以完全省略这个参数,让通用的catch块响应所有异常。C# 6引入了一个概念“异常过滤”,通过在异常类型表达式后添加when关键字来实现。如果发生了该异常类型,且过滤表达式是true,就执行catch块中的代码。
● finally——包含始终会执行的代码,如果没有产生异常,则在try块之后执行,如果处理了异常,就在catch块后执行,或者在未处理的异常“上移到调用堆栈”之前执行。“上移到调用堆栈”表示,SEH允许嵌套try…catch…finally块,可以直接嵌套,也可以在try块包含的函数调用中嵌套。例如,如果在被调用的函数中没有catch块能处理某个异常,就由调用代码中的catch块处理。如果始终没有匹配的catch块,就终止应用程序。finally块在此之前处理正是其存在的意义,否则也可以在try…catch…finally结构的外部放置代码。
在try块的代码中出现异常后,依次发生的事件如下,如图7-18所示:
● try块在发生异常的地方中断程序的执行。
● 如果有catch块,就检查该块是否匹配已抛出的异常类型。如果没有catch块,就执行finally块(如果没有catch块,就一定要有finally块)。
● 如果有catch块,但它与已发生的异常类型不匹配,就检查是否有其他catch块。
● 如果有catch块匹配已发生的异常类型,且有一个异常过滤器是true,就执行它包含的代码,再执行finally块(如果有的话)。
● 如果有catch块匹配已发生的异常类型,但没有异常过滤器,就执行它包含的代码,再执行finally块(如果有的话)。
● 如果catch块都不匹配已发生的异常类型,就执行finally块(如果有的话)。
图7-18
注意:如果存在两个处理相同异常类型的catch块,就只执行异常过滤器为true的catch块中的代码。如果还存在一个处理相同异常类型的catch块,但没有异常过滤器或异常过滤器是false,就忽略它。只执行一个catch块的代码,catch块的顺序不影响执行流。
下面用一个示例来说明异常处理。这个示例以几种方式抛出和处理异常,以便读者了解其机制。
试一试:异常处理:Ch07Ex02\Program.cs
(1)在C:\BegVCSharp\Chapter07目录中创建一个新的控制台应用程序Ch07Ex02。
(2)修改代码,如下所示(这里显示的行号注释有助于将代码与后面讨论的内容联系起来,在本章的可下载代码中也包含这些行号,以方便参考):
class Program { static string[] eTypes = { "none", "simple", "index", "nested index", "filter" }; static void Main(string[] args) { foreach (string eType in eTypes) { try { WriteLine("Main() try block reached."); // Line 21 WriteLine($"ThrowException(\"{eType}\") called."); ThrowException(eType); WriteLine("Main() try block continues."); // Line 23 } catch (System.IndexOutOfRangeException e) when (eType == "filter") { WriteLine("Main() FILTERED System.IndexOutOfRangeException" + $"catch block reached. Message:\n\"{e.Message}\"); } catch (System.IndexOutOfRangeException e) // Line 32 { WriteLine("Main() System.IndexOutOfRangeException catch " + $"block reached. Message:\n\"{e.Message}\"); } catch // Line 36 { WriteLine("Main() general catch block reached."); } finally { WriteLine("Main() finally block reached."); } WriteLine(); } ReadKey(); } static void ThrowException(string exceptionType) { WriteLine($"ThrowException(\"{exceptionType}\") reached."); switch (exceptionType) { case "none": WriteLine("Not throwing an exception."); break; // Line 57 case "simple": WriteLine("Throwing System.Exception."); throw new System.Exception(); // Line 60 case "index": WriteLine("Throwing System.IndexOutOfRangeException."); eTypes[5] = "error"; // Line 63 break; case "nested index": try // Line 66 { WriteLine("ThrowException(\"nested index\") " + "try block reached."); WriteLine("ThrowException(\"index\") called."); ThrowException("index"); // Line 71 } catch // Line 73 { WriteLine("ThrowException(\"nested index\") general" + " catch block reached."); } finally { WriteLine("ThrowException(\"nested index\") finally" + " block reached."); } break; case "filter": try // Line 86 { WriteLine("ThrowException(\"filter\") " + "try block reached."); WriteLine("ThrowException(\"index\") called."); ThrowException("index"); // Line 91 } catch // Line 93 { WriteLine("ThrowException(\"filter\") general" + " catch block reached."); throw; } break; } } }
(3)运行应用程序,结果如图7-19所示。
图7-19
示例说明
这个应用程序在Main()中有一个try块,它调用函数ThrowException()。这个函数会根据调用时使用的参数抛出异常:
● ThrowException("none")—— 不抛出异常。
● ThrowException("simple")—— 生成一般异常。
● ThrowException("index")—— 生成System.IndexOutOfRangeException异常。
● ThrowException("nested index")——包含它自己的try块,其中的代码调用ThrowException("index"),生成System.IndexOutOfRangeException异常。
● ThrowException("filter")—— 包含自己的try块,try块包含的代码调用ThrowException("index")来生成System. IndexOutOfRangeException异常,在其中异常过滤器是true。
其中的每个string参数都存储在全局数组eTypes中,在Main()函数中迭代,用每个可能的参数调用ThrowException()。在迭代过程中,会把各种信息写到控制台,说明发生了什么情况。这段代码可以使用本章前面介绍的代码单步执行技巧。在执行代码的过程中,一次执行一行代码可以确切地了解代码的执行进度。
注意:上面代码清单的步骤(2)未列出一条throw语句。这两条语句在ThrowException(string exceptionType)方法中抛出异常,以激活Main()方法中的catch块。这更好地演示了C# 6中新引入的异常过滤功能。
在代码的第21行添加一个新断点(用默认的属性),该行代码如下:
WriteLine("Main() try block reached.");
注意:这里使用了本章可下载代码中的行号来表示代码。如果关闭了行号,可以选择Tools|Options,在Text Editor | C# | General选项区域打开它们。上面的代码在注释中包含行号,这样读者在阅读这里的说明时就不需要打开文件。
在调试模式下运行应用程序。程序立即进入中断模式,此时光标停在第20行上。如果选择变量监视窗口中的Locals选项卡,就会看到eType当前是none。使用Step Into按钮处理第21和第22行,看看第一行文本是否已经写到控制台。接着使用Step Into按钮单步执行第23行的ThrowException()函数。
执行到ThrowException()函数后,Locals窗口会发生变化。eType和args超出了作用域(因为它们是Main()的局部变量),我们看到的是exceptionType局部参数,它当然是none。继续单击Step Into,到达switch语句,检查exceptionType的值,执行代码,把字符串Not throwing an exception写到屏幕上。在执行第57行上的break语句时,将退出函数,继续处理Main()中的第24行代码。因为没有抛出异常,所以继续执行try块。
接着处理finally块。再单击Step Into几次,执行完finally块和foreach的第一次循环。下次执行到第23行时,使用另一个参数simple调用ThrowException()。
继续使用Step Into单步执行ThrowException(),最终会执行到第60行:
throw new System.Exception();
这里使用C#的throw关键字生成一个异常,需要为这个关键字提供新初始化的异常作为其参数,抛出一个异常,这里使用System名称空间中的另一个异常System.Exception。
注意:在case块中使用throw时,不需要break语句,使用throw就可以结束该块的执行。
在使用Step Into执行这条语句时,将从第36行开始执行一般的catch块。因为与第26行开始的catch块都不匹配,所以执行这个一般的catch块。单步执行这段代码,然后执行finally块,最后返回到另一个循环周期,该循环在第23行用一个新参数调用ThrowException(),这次的参数是index。
这次ThrowException()在第63行生成一个异常:
eTypes[5] = "error";
eTypes是一个全局数组,所以可以在这里访问它。但是这里试图访问数组中的第6个元素(其索引从0开始计数),这会生成一个System.IndexOutOfRangeException异常。
这次Main()中有多个匹配的catch块,其中第26行的一个catch块有异常过滤器(eType =="filter"),第26行的另一个catch块没有异常过滤器。存储在eType中的值当前是"index",因此异常过滤器是false,跳过这个catch块。
单步执行到下一个catch块,从第32行开始。这个块中调用的WriteLine()使用e.Message,输出存储在异常中的消息(可以通过catch块的参数访问异常)。之后再次单步执行finally块(而不是第二个catch块,因为异常已经处理完毕)。返回循环,再次调用第23行的ThrowException()。
在执行到ThrowException()中的switch结构时,进入一个新的try块,从第66行开始。在执行到第71行时,将遇到ThrowException()的一个嵌套调用,这次使用index参数。可以使用Step Over按钮跳过其中的代码行,因为前面已经单步执行过了。与前面一样,这个调用生成一个System.IndexOutOfRangeException异常。但这个异常在ThrowException()中的嵌套try...catch...finally结构中处理。这个结构没有明确匹配这种异常的catch块,所以执行一般的catch块(从第73行开始)。
继续单步执行代码,这次到达ThrowException()中的switch结构时,进入一个新的try块,从第86行开始。到达第91行时,和以前一样,执行一个嵌套调用ThrowException()。但是,这次处理Main()中System. IndexOutOfRangeException异常的catch块会检查过滤表达式(eType = = "filter"),其结果是true,所以执行该catch块,不是处理System. IndexOutOfRangeException的、没有异常过滤器的catch块。
与前面的异常处理一样,现在单步执行这个catch块,以及关联的finally块,最后返回到函数调用的末尾处。但是它们有一个重要区别:抛出的异常是由ThrowException()中的代码处理的。这就是说,异常并没有留给Main()处理,所以直接进入finally块,之后应用程序中断执行。
7.2.2 列出和配置异常
.NET Framework包含许多异常类型,可以在代码中自由抛出和处理这些类型的异常。IDE提供了一个Exceptions对话框,可以检查和编辑可用的异常。使用Debug | Exceptions菜单项(或按下Ctrl+D, E)可打开该对话框,如图7-20所示。
该对话框按照类别和.NET库名称空间列出异常。展开Common Language Runtime Exceptions的加号,就可以看到System名称空间中的异常,这个列表包括上面使用的System.IndexOutOf-RangeException异常。
每个异常都可以使用右边的复选框来配置。使用(break when)Thrown时,即使是对于已处理的异常,也会进入调试器。
图7-20