通用语言运行时(CLR)具有的一个很大的优势为异常处理是跨语言被标准化的。一个在C#中所引发的异常可以在Visual Basic客户中得到处理。不再有 HRESULTs 或者 ISupportErrorInfo 接口。 数据挖掘工具
尽管跨语言异常处理的覆盖面很广,但这一章完全集中讨论C#异常处理。你稍为改变编译器的溢出处理行为,接着有趣的事情就开始了:你处理了该异常。要增加更多的手段,随后引发你所创建的异常。
7.1 检查和非检查语句(checked and unchecked statements) 数据挖掘交友
当你执行运算时,有可能会发生计算结果超出结果变量数据类型的有效范围。这种情况被称为溢出,依据不同的编程语言,你将被以某种方式通知——或者根本就没有被通知。(C++程序员听起来熟悉吗?)
那么,C#如何处理溢出的呢? 要找出其默认行为,请看我在这本书前面提到的阶乘的例子。(为了方便其见,前面的例子再次在清单 7.1 中给出)
数据挖掘实验室
数据挖掘交友
清单 7.1 计算一个数的阶乘 数据挖掘研究院
数据挖掘工具
1: using System; 数据挖掘研究院
2:
数据挖掘实验室
3: class Factorial 数据挖掘实验室
4: { 数据挖掘论坛
5: public static void Main(string[] args) 数据挖掘交友
6: { 数据挖掘研究院
7: long nFactorial = 1; 数据挖掘交友
8: long nComputeTo = Int64.Parse(args[0]);
数据挖掘研究院
9: 数据挖掘交友
10: long nCurDig = 1; 数据挖掘交友
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
数据挖掘实验室
12: nFactorial *= nCurDig; 数据挖掘研究院
13: 数据挖掘交友
14: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
15: }
16: }
数据挖掘论坛
数据挖掘交友
当你象这样使用命令行执行程序时 数据挖掘工具
factorial 2000
结果为0,什么也没有发生。因此,假定C#默默地处理溢出情况而不明确地警告你是安全的。
数据挖掘工具
通过对整个应用程序(经编译器开关)或在语句级允许溢出检查,你就可以改变这种行为。以下两节分别解决一种方案。 数据挖掘研究院
7.1.1溢出检查的编译器设置 数据挖掘论坛
如果你想控制整个应用程序的溢出检查,C#编译器设置选项正是你所要找的。默认地,溢出检查是禁用的。要明确地请求它,运行以下编译器命令:
csc factorial.cs /checked+
数据挖掘交友
现在当你用2000参数执行应用程序时,CLR通知你溢出异常(见图 7.1)。 数据挖掘论坛
数据挖掘交友
图 7.1 允许了溢出异常,阶乘代码产生了一个异常。
数据挖掘实验室
数据挖掘研究院
数据挖掘研究院
按确定键离开对话框出现了异常信息: 数据挖掘论坛
Exception occurred: System.OverflowException
数据挖掘研究院
at Factorial.Main(System.String[]) 数据挖掘实验室
数据挖掘论坛
现在你了解了溢出条件引发了一个 System.OverflowException异常。下一节,在我们完成语法检查之后,如何捕获并处理所出现的异常? 数据挖掘论坛
7.1.2 语法溢出检查 数据挖掘交友
如果你不想对整个应用程序进行溢出检查,那么只允许对某些代码段进行检查便可,这样可能会很顺畅。在这种场合下,你可能象清单7.2中显示的那样使用检查语句。
数据挖掘研究院
清单 7.2 阶乘计算中的溢出检查 数据挖掘实验室
数据挖掘实验室
1: using System; 数据挖掘论坛
2: 数据挖掘实验室
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
数据挖掘工具
7: long nFactorial = 1; 数据挖掘交友
8: long nComputeTo = Int64.Parse(args[0]); 数据挖掘交友
9: 数据挖掘研究院
10: long nCurDig = 1;
数据挖掘论坛
11:
数据挖掘论坛
12: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
13: checked { nFactorial *= nCurDig; } 数据挖掘实验室
14:
15: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial); 数据挖掘实验室
16: } 数据挖掘工具
17: }
数据挖掘交友
数据挖掘研究院
纵使你运用标志 checked- 编译了该代码,在第13行中,溢出检查仍然会对乘法实施检查,因为checked 语句已经括住了它。将会出现相同的错误信息。
显示相反行为的语句是unchecked 。甚至如果允许了溢出检查(给编译器加上checked+标志),被unchecked 语句所括住的代码也将不会引发溢出异常: 数据挖掘交友
数据挖掘研究院
unchecked
{
nFactorial *= nCurDig; 数据挖掘论坛
}
数据挖掘实验室
7.2 异常处理语句
数据挖掘论坛
既然你知道了如何产生一个异常(你会发现更多的方法,相信我),仍然存在如何处理它的问题。如果你是一个 C++ WIN32 程序员,肯定熟悉SEH(结构异常处理)。令人感到欣慰的是,C#中的命令几乎是相同的,而且它们也以相似的方式运作。
数据挖掘实验室
数据挖掘实验室
以下三节介绍了C#的异常处理语句:
数据挖掘论坛
。用 try-catch 捕获异常 数据挖掘研究院
。用try-finally 清除异常 数据挖掘工具
。用try-catch-finally 处理所有的异常 数据挖掘研究院
数据挖掘实验室
7.2.1 使用 try 和 catch捕获异常
数据挖掘研究院
你肯定会对一件事非常感兴趣——不要给用户提示那些令人讨厌的异常消息,以便你的应用程序继续执行。要这样,你必须捕获(处理)该异常。 数据挖掘研究院
要用到的语句是try 和 catch。try包含可能会产生异常的语句,而catch处理一个异常,如果有异常存在的话。清单7.3 用try 和 catch为OverflowException 实现异常处理。 数据挖掘论坛
数据挖掘交友
清单7.3 捕获由 Factorial Calculation引发的OverflowException 异常
数据挖掘实验室
1: using System;
2:
数据挖掘论坛
3: class Factorial 数据挖掘交友
4: { 数据挖掘工具
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1; 数据挖掘研究院
8: long nComputeTo = Int64.Parse(args[0]);
9: 数据挖掘研究院
10: try 数据挖掘研究院
11: { 数据挖掘工具
12: checked
13: { 数据挖掘实验室
14: for (;nCurDig <= nComputeTo; nCurDig++) 数据挖掘论坛
15: nFactorial *= nCurDig;
数据挖掘交友
16: }
17: } 数据挖掘工具
18: catch (OverflowException oe) 数据挖掘交友
19: { 数据挖掘论坛
20: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
21: return;
22: } 数据挖掘研究院
23:
数据挖掘论坛
24: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
25: } 数据挖掘论坛
26: } 数据挖掘工具
数据挖掘交友
为了说明清楚,我扩展了某些代码段,而且我也保证异常是由checked 语句产生的,甚至当你忘记了编译器设置时。 数据挖掘工具
正如你所见,异常处理并不麻烦。你所有要做的是:在try语句中包含容易产生异常的代码,接着捕获异常,该异常在这个例子中是OverflowException类型。无论一个异常什么时候被引发,在catch段里的代码会注意进行适当的处理。 数据挖掘论坛
如果你不事先知道哪一种异常会被预期,而仍然想处于安全状态,简单地忽略异常的类型。
数据挖掘实验室
try 数据挖掘论坛
{ 数据挖掘工具
...
数据挖掘工具
}
catch 数据挖掘研究院
{ 数据挖掘研究院
... 数据挖掘工具
}
数据挖掘工具
但是,通过这个途径,你不能获得对异常对象的访问,而该对象含有重要的出错信息。一般化异常处理代码象这样:
数据挖掘工具
数据挖掘工具
try
数据挖掘交友
{ 数据挖掘研究院
...
数据挖掘交友
} 数据挖掘论坛
catch(System.Exception e) 数据挖掘交友
{
...
}
数据挖掘交友
数据挖掘实验室
注意,你不能用 ref 或 out 修饰符传递 e 对象给一个方法,也不能赋于它一个不同的值。
数据挖掘研究院
7.2.2 使用 try 和 finally 清除异常 数据挖掘论坛
如果你更关心清除而不是错误处理, try 和 finally 会获得你的喜欢。尽管它并没有抑制出错信息,但包含在 finally 块中的代码在异常被引发后仍然会被执行。 数据挖掘研究院
尽管程序不正常终止,但你还可以给用户提供一条消息,如清单 7.4 所示。 数据挖掘实验室
数据挖掘实验室
清单 7.4 在finally 语句中处理异常 数据挖掘论坛
1: using System; 数据挖掘实验室
2: 数据挖掘研究院
3: class Factorial 数据挖掘工具
4: { 数据挖掘论坛
5: public static void Main(string[] args)
6: { 数据挖掘交友
7: long nFactorial = 1, nCurDig=1; 数据挖掘交友
8: long nComputeTo = Int64.Parse(args[0]); 数据挖掘研究院
9: bool bAllFine = false; 数据挖掘研究院
10:
数据挖掘实验室
11: try
数据挖掘交友
12: {
13: checked 数据挖掘工具
14: { 数据挖掘论坛
15: for (;nCurDig <= nComputeTo; nCurDig++)
数据挖掘实验室
16: nFactorial *= nCurDig; 数据挖掘交友
17: } 数据挖掘实验室
18: bAllFine = true; 数据挖掘交友
19: } 数据挖掘交友
20: finally 数据挖掘研究院
21: {
数据挖掘研究院
22: if (!bAllFine) 数据挖掘论坛
23: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
24: else 数据挖掘研究院
25: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial); 数据挖掘交友
26: }
数据挖掘交友
27: }
28: } 数据挖掘交友
数据挖掘工具
通过测试该代码,你可能会猜到,即使没有引发异常处理,finally也会被执行。这是真的——在finally中的代码总是会被执行的,不管是否具有异常条件。为了举例说明如何在两种情况下提供一些有意义的信息给用户, 我引进了新变量bAllFine。bAllFine告诉finally 语块,它是否因为一个异常或者仅是因为计算的顺利完成而被调用。 数据挖掘论坛
作为一个习惯了SEH程序员,你可能会想,是否有一个与C++中很管用的__leave 语句等价的语句。如果你还不了解,这里说明一下:在C++中的__leave 语句是用来提前终止 try 语段中的执行代码,并立即跳转到finally 语段 。
数据挖掘实验室
坏消息! C# 中并没有__leave 语句。但是,在清单 7.5 中的代码演示了一个你可以实现的方案。 数据挖掘工具
清单 7.5 从 try语句 跳转到finally 语句
数据挖掘实验室
1: using System; 数据挖掘实验室
2:
数据挖掘工具
3: class JumpTest 数据挖掘工具
4: {
5: public static void Main()
数据挖掘研究院
6: { 数据挖掘工具
7: try 数据挖掘交友
8: {
数据挖掘实验室
9: Console.WriteLine("try");
10: goto __leave; 数据挖掘实验室
11: } 数据挖掘实验室
12: finally 数据挖掘工具
13: {
14: Console.WriteLine("finally"); 数据挖掘工具
15: }
16: 数据挖掘工具
17: __leave: 数据挖掘实验室
18: Console.WriteLine("__leave"); 数据挖掘交友
19: } 数据挖掘交友
20: } 数据挖掘实验室
数据挖掘研究院
数据挖掘实验室
当这个应用程序运行时,输出结果为 数据挖掘实验室
数据挖掘论坛
try
数据挖掘论坛
finally 数据挖掘实验室
__leave 数据挖掘实验室
一个 goto 语句不能退出 一个finally 语块。甚至把 goto 语句放在 try 语句块中,还是会立即返回控制到 finally 语块。因此,goto 只是离开了 try 语块并跳转到finally 语块。直到 finally 中的代码完成运行后,才能到达__leave 标签。按这种方式,你可以模仿在SEH中使用的的__leave 语句。 数据挖掘研究院
顺便地,你可能怀疑goto 语句被忽略了,因为它是try 语句中的最后一条语句,并且控制自动地转移到了 finally 。为了证明不是这样,试把goto 语句放到Console.WriteLine 方法调用之前。尽管由于存在着不可到达代码,使你得到了编译器的警告,但是你将看到goto语句实际上被执行了,而没有产生“try”字符串的输出。 数据挖掘交友
数据挖掘工具
7.2.3 使用try-catch-finally处理所有异常
数据挖掘实验室
应用程序最有可能的途径是合并前面两种错误处理技术——捕获错误、清除并继续执行应用程序。所有你要做的是在出错处理代码中使用 try 、catch 和 finally语句。清单 7.6 显示了处理零除错误的途径。
数据挖掘工具
数据挖掘工具
清单 7.6 实现多个catch 语句
数据挖掘论坛
1: using System;
数据挖掘工具
2: 数据挖掘论坛
3: class CatchIT 数据挖掘工具
4: {
数据挖掘论坛
5: public static void Main()
6: { 数据挖掘论坛
7: try
数据挖掘论坛
8: {
数据挖掘工具
9: int nTheZero = 0;
10: int nResult = 10 / nTheZero; 数据挖掘工具
11: }
数据挖掘实验室
12: catch(DivideByZeroException divEx) 数据挖掘实验室
13: { 数据挖掘论坛
14: Console.WriteLine("divide by zero occurred!"); 数据挖掘工具
15: } 数据挖掘交友
16: catch(Exception Ex) 数据挖掘交友
17: {
18: Console.WriteLine("some other exception"); 数据挖掘交友
19: } 数据挖掘研究院
20: finally 数据挖掘实验室
21: { 数据挖掘交友
22: }
23: }
24: }
数据挖掘论坛
这个例子的技巧为,它包含了多个catch 语句。第一个捕获了更可能出现的DivideByZeroException异常,而第二个catch语句通过捕获普通异常处理了所有剩下来的异常。 数据挖掘工具
你肯定总是首先捕获特定的异常,接着是普通的异常。如果你不按这个顺序捕获异常,会发生什么事呢?清单7.7中的代码有说明。 数据挖掘交友
数据挖掘研究院
清单7.7 顺序不适当的 catch 语句 数据挖掘论坛
数据挖掘交友
1: try
2: {
3: int nTheZero = 0; 数据挖掘研究院
4: int nResult = 10 / nTheZero;
数据挖掘研究院
5: } 数据挖掘研究院
6: catch(Exception Ex)
数据挖掘工具
7: {
数据挖掘研究院
8: Console.WriteLine("exception " + Ex.ToString());
9: } 数据挖掘论坛
10: catch(DivideByZeroException divEx)
数据挖掘研究院
11: { 数据挖掘论坛
12: Console.WriteLine("never going to see that");
数据挖掘研究院
13: }
数据挖掘实验室
数据挖掘工具
编译器将捕获到一个小错误,并类似这样报告该错误: 数据挖掘工具
wrongcatch.cs(10,9): error CS0160: A previous catch clause already 数据挖掘工具
catches all exceptions of this or a super type (′System.Exception′)
数据挖掘实验室
数据挖掘实验室
意思为: 数据挖掘论坛
wrongcatch.cs(10,9): 错误代码 CS0160: 前面的catch语句早已捕获了这个或高级类型(′System.Exception′)的所有异常。
最后,我必须报导CLR异常与SEH相比时的一个缺点(或差别):没有在SEH异常过滤器中很有用的EXCEPTION_CONTINUE_EXECUTION标识符的等价物。基本上,EXCEPTION_CONTINUE_EXECUTION 允许你重新执行负责异常的代码片段。在重新执行之前,你有机会更改变量等。我个人特别喜欢的技术为:通过使用存取违例异常,按需要实施内存分配。 数据挖掘研究院
数据挖掘实验室
7.3 引发异常
当你不得不捕获异常时,其他人首先必须首先能够引发异常。而且,不仅其他人能够引发,你也可以负责引发。其相当简单:
数据挖掘交友
throw new ArgumentException("Argument can′t be 5"); 数据挖掘研究院
你所需要的是throw 语句和一个适合的异常类。我已经从表7.1提供的清单中为这个例子选出一个异常。 数据挖掘论坛
表 7.1 Runtime提供的标准异常
异常类型
描述
Exception
所有异常对象的基类
SystemException
运行时产生的所有错误的基类
IndexOutOfRangeException
当一个数组的下标超出范围时在运行时被引发
NullReferenceException
当一个空对象被引用时在运行时被引发
InvalidOperationException
当对方法的调用对对象的当前状态无效时,由某些方法引发
ArgumentException
所有参数异常的基类
ArgumentNullException
在参数为空(不允许)的情况下,由方法引发。
ArgumentOutOfRangeException
当参数不在一个给定范围之内时,由方法引发
InteropException
目标在或发生在CLR外面的异常的基类
ComException
包含COM 类的HRESULT信息的异常
SEHException
封装win32 结构异常处理信息的异常
数据挖掘工具
数据挖掘交友
然而,在catch语句的内部,你已经有了随意处置的异常,就不必创建一个新异常。可能在表7.1 中的异常没有一个符合你特殊的要求——为什么不创建一个新的异常?在即将要学到小节中,都涉及到这两个话题。 数据挖掘交友
7.3.1 重新引发异常
当处于一个catch 语句的内部时,你可能决定引发一个目前正在再度处理的异常,进一步的处理留给一些外部的try-catch 语句。该方法的例子如 清单7.8所示。 数据挖掘研究院
数据挖掘交友
清单 7.8 重新引发一个异常
数据挖掘研究院
1: try
数据挖掘研究院
2: {
3: checked 数据挖掘交友
4: { 数据挖掘论坛
5: for (;nCurDig <= nComputeTo; nCurDig++) 数据挖掘论坛
6: nFactorial *= nCurDig;
数据挖掘论坛
7: } 数据挖掘研究院
8: }
数据挖掘实验室
9: catch (OverflowException oe)
10: {
11: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
12: throw;
13: }
数据挖掘交友
注意,我不必规定所声明的异常变量。尽管它是可选的,但你也可以这样写: 数据挖掘工具
throw oe;
数据挖掘研究院
现在我们还必须留意这个异常。
数据挖掘实验室
7.3.2 创建自己的异常类
数据挖掘工具
尽管建议使用预定义的异常类,但在实际场合,创建自己的异常类可能会很方便。创建自己的异常类,允许你的异常类的使用者根据该异常类采取不同的手段。 数据挖掘交友
在清单 7.9 中出现的异常类 MyImportantException遵循两个规则:第一,它用Exception结束类名。第二,它实现了所有三个被推荐的通用结构。你也应该遵守这些规则。 数据挖掘论坛
清单 7.9 实现自己的异常类 MyImportantException
数据挖掘工具
1: using System;
2: 数据挖掘论坛
3: public class MyImportantException:Exception
4: {
数据挖掘实验室
5: public MyImportantException()
数据挖掘交友
6: :base() {}
7:
8: public MyImportantException(string message)
数据挖掘论坛
9: :base(message) {} 数据挖掘工具
10:
数据挖掘研究院
11: public MyImportantException(string message, Exception inner)
12: :base(message,inner) {} 数据挖掘交友
13: } 数据挖掘交友
14: 数据挖掘工具
15: public class ExceptionTestApp 数据挖掘研究院
16: { 数据挖掘交友
17: public static void TestThrow()
数据挖掘工具
18: { 数据挖掘论坛
19: throw new MyImportantException("something bad has happened.");
20: } 数据挖掘研究院
21:
22: public static void Main()
数据挖掘实验室
23: { 数据挖掘实验室
24: try 数据挖掘工具
25: {
数据挖掘交友
26: ExceptionTestApp.TestThrow(); 数据挖掘交友
27: }
数据挖掘研究院
28: catch (Exception e)
29: { 数据挖掘论坛
30: Console.WriteLine(e); 数据挖掘实验室
31: }
数据挖掘论坛
32: }
33: } 数据挖掘交友
数据挖掘实验室
正如你所看到的,MyImportantException 异常类不能实现任何特殊的功能,但它完全基于System.Exception类。使用一条catch 语句于System.Exception 类,程序的剩余部分测试新的异常类。 数据挖掘交友
如果没有特殊的实现而只是给MyImportantException定义了三个构造函数,创建它又有什么意义呢?它是一个重要的类型——你可以在catch语句中使用它,代替更为普通的异常类。可能引发你的新异常的客户代码可以按规定的catch代码起作用。 数据挖掘工具
当使用自己的名字空间编写一个类库时,也要把异常放到该名字空间中。尽管它并没有出现在这个例子中,你还是应该使用适当的属性,为扩展了的错误信息扩充你的异常类。 数据挖掘实验室
7.4 异常处理的“要”和“不要” 数据挖掘实验室
作为最后的忠告之语,这里是对异常引发和处理所要做和不要做的清单:
。要提供有意义的文本,当引发异常时。 数据挖掘研究院
。要引发异常,仅当条件是真正异常,也就是当一个正常的返回值不满足时。
。要引发一个ArgumentException异常, 如果你的方法或属性被传递一个坏参数时。 数据挖掘交友
。要引发一个 InvalidOperationException异常,当调用操作不适合对象的当前状态时。 数据挖掘实验室
。要引发最适合的异常。 数据挖掘工具
。要使用链表异常,它们允许你跟踪异常树。 数据挖掘研究院
。不要为正常或预期的错误使用异常。 数据挖掘交友
。不要为流程的正常控制使用异常。
。不要在方法中引发 NullReferenceException或IndexOutOfRangeException异常。
数据挖掘论坛
7.5 小结
数据挖掘论坛
这一章由介绍溢出检查开始。你可以使用编译器开关(默认是关),使整个应用程序允许或禁止溢出检查。如果需要微调控制,你可以使用检查和非检查语句,它允许你用或不用溢出检查来执行一段代码,尽管没有设置应用程序开关。
当发生溢出时,一个异常就被引发了。如何处理异常取决于你。我提到了各种途径,包括你最有可能贯穿整个应用程序中使用的:try、catch 和finally 语句。在伴随的多个例子中,你学到了它与WIN32结构异常处理(SEH)的差别。
异常处理供类的用户使用; 然而,如果你负责创建新的类,就可以引发异常。有多种选择:引发早已捕获的异常,引发存在的框架异常,或者按规定的实际目标创建新的异常类。
数据挖掘工具
最后,你需要阅读引发和处理异常的每一条“要”和“不要”。 数据挖掘实验室
数据挖掘实验室