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