C#语言包含** 结构化异常处理** (Structured Exception Handling, ** SEH** )的语法。用 3 个关键字可以标记出能处理异常的代码和指令,如果发生异常,就使用这些指令处理异常。用于这个目的的 3 个关键字是 try、catch和 finally。它们都有一个关联的代码块,必须在连续的代码行中使用。其基本结构如下:
try { ... } catch (<exceptionType> e) { ... } finally { ... }
也可以只有 try 块和 finally 块,而没有 catch 块,或者有一个 try 块和好几个 catch 块。如果有一个或多个 catch 块,finally 块就是可选的,否则就是必需的。这些代码块的用法如下:
●** try** --包含抛出异常的代码(在谈到异常时,C#语言用 “抛出”这个术语表示 “生成” 或 “导致”)。
●** catch** --包含抛出异常时要执行的代码。catch 块可以使用 <exceptionType > ,设置为只响应特定的异常类型(如System.IndexOutOfRangeException),以便提供多个 catch 块。还可以完全省略这个参数,让一般的 catch 块响应所有异常。
●** finally** --包含始终会执行的代码,如果没有产生异常,则在 try 块之后执行,如果处理了异常,就在 catch 块后执行,或者在未处理的异常上移到调用堆栈之前执行。“上移到调用堆栈”表示,SEH允许嵌套 try...catch...finally 块,可以直接嵌套,也可以在 try 块包含的函数调用中嵌套。例如,如果在被调用的函数中没有 catch 块能处理某个异常,就由调用代码中的 catch 块处理。如果始终没有匹配的 catch 块,就终止应用程序。finally 块在此之前处理正是其存在的意义,否则也可以在 try...catch...finally 结构的外部放置代码。这个嵌套功能将在后面的 “异常处理的注意事项” 一节中进一步讨论,所以如果对这个功能感到困惑不解,请不必担心。
在 try 块的代码中出现异常后,发生的事件依次是:
● try 块在发生异常的地方中断程序的执行。
● 如果有 catch 块,就检查该块是否匹配已抛出的异常类型。如果没有 catch 块,就执行 finally 块(如果没有 catch 块,就一定要有 finally 块)。
● 如果有 catch 块,但它与已发生的异常类型不匹配,就检查是否有其他 catch 块。
● 如果有 catch 块匹配已发生的异常类型,就执行它包含的代码,再执行 finally 块(如果有)。
● 如果 catch 块都不匹配已发生的异常类型,就执行 finally 块(如果有)。
下面用一个示例来说明异常的处理。这个示例以几种方式抛出和处理异常,以便读者了解其机制。
修改代码,如下所示(这里显示的行号注释有助于将代码与后面讨论的内容联系起来,在本章的可下载代码中也包含这些行号,以方便参考):
class Program { static string[] eTypes = { "none", "simple", "index", "nested index" }; static void Main(string[] args) { foreach(string eType in eTypes) { try { Console.WriteLine("Main() try block reached."); // Line 19 Console.WriteLine("ThrowException(\"{0}\") called.", eType); ThrowException(eType); Console.WriteLine("Main() try block continues."); // Line 22 } catch (System.IndexOutOfRangeException e) // Line 24 { Console.WriteLine("Main() System.IndexOutOfRangeException catch" + "block reached.Message:\n\"{0}\"", e.Message); } catch // Line 30 { Console.WriteLine("Main() general catch block reached."); } finally { Console.WriteLine("Main() finally block reached."); } Console.WriteLine(); } Console.ReadKey(); } static void ThrowException(string exceptionType) { Console.WriteLine("ThrowException(\"{0}\") reached.", exceptionType); switch (exceptionType) { case "none": Console.WriteLine("Not throwing an exception."); break; // Line 50 case "simple": Console.WriteLine("Throwing System.Exception."); throw new System.Exception(); // Line 53 case "index": Console.WriteLine("Throwing System.IndexOutOfRangeException."); eTypes[4] = "error"; // Line 56 break; case "nested index": try // Line 59 { Console.WriteLine("ThrowException(\"nested index\")" + "try block reached."); Console.WriteLine("ThrowException(\"index\") called."); ThrowException("index"); // Line 64 } catch // Line 66 { Console.WriteLine("ThrowException(\"nested index\") general" + "catch block reached."); } finally { Console.WriteLine("ThrowException(\"nested index\") finally" + "block reached."); } break; } } }
示例的说明 这个应用程序在
Main()中有一个try块,它调用函数ThrowException()。这个函数会根据调用时使用的参数抛出异常:
● ThrowException("none") --不抛出异常。 ● ThrowException("simple") --生成一般异常。 ● ThrowException("index") --生成
System.IndexOutOfRangeException异常。 ● ThrowException("nested index") --包含它自己的try块,其中的代码调用ThrowException("index"),生成一个System.IndexOutOfRangeException异常
其中的每个
string参数都存储在全局数组eTypes中,在Main()函数中迭代,用每个可能的参数调用ThrowException()。在迭代过程中,会把各种信息写到控制台上,说明发生了什么情况。这段代码可以使用本章节前面介绍的代码单步执行技巧。在执行代码的过程中,一次执行一行代码可以确切地了解代码的执行进度。
在代码的第 19 行添加一个新断点(用默认的属性),该行代码如下:
Console.WriteLine("Main() try block reached.");
这里使用了行号来表示代码。如果关闭了行号,可以选择
工具(T)|选项(O),在文本编辑器|C#|常规选项区域中打开它们。
在调试模式下运行应用程序。程序立即进入中断模式,此时光标停在第 19 行上。如果选择变量监视窗口中的
局部变量选项卡,就会看到eType当前是none。使用逐语句按钮处理第 19 和 20 行,看看第一行文本是否已经写到控制台。接着使用逐语句按钮单步执行第 21 行的ThrowException()函数。
执行到
ThrowException()函数后,局部变量窗口会发生变化。eType和args超出了作用域(因为它们是Main()的局部变量),我们看到的是exceptionType的值,执行代码,把字符串Not throw an exception写到屏幕上。在执行第 50 行上的break语句时,将退出函数,继续处理Main()中的第 22 行代码。因为没有抛出异常,所以继续执行try块。
接着处理
finally块。再单击逐语句几次,执行完finally块和foreach的第一次循环。下次执行到第 21 行时,使用另一个参数simple调用ThrowException()。 继续使用逐语句单步执行ThrowException(),最终会执行第 53 行:
throw new System.Exception();
这里使用C#
throw关键字生成一个异常,需要为这个关键字提供新初始化的异常作为其参数,抛出一个异常,这里使用名称空间System中的另一个异常System.Exception。
这个
case块不需要break语句,使用throw就可以结束该块的执行。
在使用
逐语句执行这个语句时,将从第 30 行开始执行一般的catch块。因为与第 24 行开始的catch块都不匹配,所以执行这个一般的catch块。单步执行这段代码,然后执行finally块,最后返回到另一个循环周期,该循环在第 21 行用一个新参数调用ThrowException(),这次的参数是index。 这次ThrowException()在第 56 行生成一个异常:
eType[4] = "error";
eTypes是一个全局数组,所以可以在这里访问它。但是这里视图访问数组中的第 5 个元素(其索引从 0 开始计数),这会生成一个System.IndexOutOfRangeException异常。
这次
Main()中有一个匹配的catch块,单步执行该catch块,从第 24 行开始。这个块中调用的Console.WriteLine()使用e.Message,输出存储在异常中的消息(可以通过catch块的参数访问异常)。之后再次单步执行finally块(而不是第二个catch块,因为异常已经处理完毕)。返回循环,再次调用第 21 行的 ThrowException()。
在执行到
ThrowException()中的switch结构时,进入一个新的try块,从第 59 行开始。在执行到第 64 行时,将遇到ThrowException()的一个嵌套调用,这次使用index参数。可以使用逐过程按钮跳过其中的代码行,因为前面已经单步执行过了。与前面一样,这个调用生成一个System.IndexOutOfRangeException异常。但这个异常在ThrowException()中的嵌套try...catch...finally结构中处理。这个结构没有明确匹配这种异常catch块,所以执行一般的catch块(从第 66 行开始)。
与前面的异常处理一样,现在单步执行这个
catch块,以及关联的finally块,最后返回到函数调用的末尾处。但是它们有一个重要的区别:抛出异常是由ThrowException()中的代码处理的。这就是说,异常并没有留给Main()处理,所以直接进入finally块,之后应用程序中断执行。