声明:本CSDN博客中的所有文章均为本人原创 请勿转载
C#中的异常处理
一.什么是异常?
异常是非预期的,超出程序处理能力的情况。
常见的异常类型有:空指针引用,栈溢出,算数异常,格式转换异常,数据越界等。
在.netframework中,当代码中出现异常时,控制权将交给CLR,CLR将构造与内置异常类型相关的对象。这些对象是具体的,对应于相关的异常类型。
这些异常类都继承自System.Exception命名空间下的Exception.该类是异常类型的基类。.net提供一个公共异常模型,异常处理在CLR中实现。这说明在一种托管语言引发的异常可以被其它语言捕获并处理。
二.深解异常处理机制。
.netframework提供了结构化的异常处理。从而可以在程序中处理异常而不会被抛向JIT。
1. TRY CATCH 块的结构。
语法:try{/*可能引发异常的代码*/ }catch(ExceptionType ExObj){/*异常处理*/ }finally{/*不论是否异常,都要执行的代码*/}
一个TRY块必须和一个或多个Catch块成对使用,当有多个Catch块时,用于捕获语句中某种类型的异常或多种类型的异常对象,不论哪种情况,都要将子类异常类型放在前面匹配,父类置后以处理更多情况,否则将造成不可到达的代码。在有finally块的情况下才可以省略Catch 块,这意味着异常发生后仅执行finally块代码。
finally不是必须的。在有finally块的情况下,才允许TRY没有catch块。
[Try代码块]可以认为TRY块是一个监视器,TRY块内的代码受到异常保护。TRY在异常处理机制中是必有的。
一旦TRY块中发生异常,CRL获取控制权收缩堆栈,生成具有高优先级的异常对象,如果找到匹配的Catch块便执行Catch块的语句,CRL又将堆栈展开Catch对应的那个范围以继续执行。
[Catch]
Catch语句用来筛选和处理异常。
Catch后的参数表为具体异常类型的对象。用来匹配TRY中引发的异常对象。一旦匹配成功,才执行catch块中的代码,后面的catch块将不会再匹配或执行。如果不匹配,将继续查找下一个catch来匹配。
[finally]
Finally块是终结处理器。不论对应TRY块内异常是否引发,它将都被执行。这说明最好将异常发生或不发生时都需要执行的代码放入finally块中,它常用来释放资源。
( 详细的异常处理过程。)
2.THROW 语句。
Throw语句可以不经过CRL构造而手动抛出异常。
3.THROWS语句显式标记函数会抛出异常,所以在调用该函数时编译器将强制检查对此函数调用时的异常处理。所以你在调用时必须对该异常进行处理。
[异常的抛出与系统对异常的处理过程]
Try-catch语句使程序在处理完异常后可以继续执行该结构后的正常代码。
异常上抛原理:当函数中引发异常时,将产生异常对象,并抛向该方法的调用点,如果调用的方法中对此异常没有处理(即没有能匹配的try—catch块),异常对象将继续上抛,直到被catch匹配。如果没有相关匹配,异常会最终到Main方法。Main方法会抛给JIT,最终变为不可处理的异常。由一个发送与不发送的窗口来显示。
需要注意的是,在处理地点的选择上下文将有说明,但就地处理和在高层处理的另一个重要区别是,高层处理将使上浮所经过的方法调用点以下的代码不能被执行。
三.异常的正确处理。
异常会导致后续代码不能被执行。这种问题很容易使程序的数据完整性遭到破坏。对一个独立的执行单元来说,应当使用事务机制回滚异常造成的不完整修改。
1.几种异常处理所犯的错误。
资源修改而没有恢复。这不仅仅是资源释放的问题。你还要对程序不完整执行所造成的影响心中有数。 如执行安装文件没有成功时,一些优秀的程序设计将自动回滚,以撤消对系统的更改。
2.将错误用处理语句掩盖,使程序功能不完整而失效(如例3)。
.netframework提供的异常处理机制会使异常不会终止应用程序,而允许设计人员进行处理。这在一个考虑不全面的程序设计人员来说并非是一件好事。这就说明,如果对异常只是简单捕捉和返回,而让其继续执行就意味着有抛出异常的方法没有完全完成自己的功能,而你恰恰没有合适的方式处理。这种现象造成的问题在大型程序中将非常突出。
《正确处理异常的方法》
异常的外跳和继续对功能逻辑带来的问题。
假如我们的午餐需要做一份煎鸡蛋,其中包括三道制作工序,一,准备鸡蛋,油。二加热油,加入调料 三 烹制,完成制作。
在程序中,可能一个功能的完成依靠许多方法的共同协作。这些方法之间具有一定的依赖性,如果某一个方法出现异常,程序设计者必需考虑到怎样完美的处理这种问题。下面的程序模拟了这个过程:
class myLunch{
public void doEeg()
{
step_1();
try
{
step_2();
}
catch (Exception ex)
{
}
step_3();
}
private void step_1()
{
Console.WriteLine("(一)准备鸡蛋,油。");
return;
}
private void step_2()
{
throw new Exception("找不到调料");
Console.WriteLine("(二)加热油,加入调料");
return;
}
private void step_3()
{
Console.WriteLine("(三)烹制,完成制作。");
return;
}
}
上面的代码存在许多问题。一般的在catch块中仅输出异常的方式是十分不理智的,这种方式只能用来调试时使用。由于catch块使得代码不被终断,从而掩盖了方法的不完整性执行。可以在doEeg()方法调用点上捕捉异常,但无论如何,step_1()都会被执行。这一点必须在清理影响时被考虑到。 程序越庞大,即方法嵌套层次越深,异常处理越复杂。
第一.确定这异常的影响范围。这要根据程序中哪些部分依赖于该函数的执行来确定。
尤其是在一些大型程序设计中,可能某个人员只负责其中的某个模块,这样很难确定自己代码中出现的异常对整个程序会造成什么影响。所以,有些有隐患的异常需要向上报告。以便从整体上给予处理。例如上面的例子中,如果煎鸡蛋的第二步出了故障,你不能仅清理了影响就算是完了,你还要知道,如果你做的这道菜是一顿午餐的一部分,是不是有可能接下来别人做自己的菜时会不会用到你的煎鸡蛋(例3)。无疑你的煎蛋是失败的,一旦别人使用,又会引发一连串异常需要去考虑。较好的方法是你以某种方式告诉调用点,这一步骤执行失败,或者向上抛出异常,等待上一级处理。
第二.在适当的地点对其进行处理。应该在什么地方对异常进行处理呢,一般有就地处理,向上抛出两种方式,这里所说的处理就是清理影响,上报异常,而不是向上抛出。我认为,对于影响功能完整性的异常,最好在一个独立的执行单元的最高结点上进行处理。这个结点实际上包含了异常影响范围。 如果doEgg()方法是一个独立的执行单元,在调用点捕捉并处理显的很直观和简单,我们很容易知道,煎鸡蛋失败了,而不需要太关心是那个步骤失败,然后需要我们去清理现场。可见,上面代码中的处理地点是错误的。它无疑埋下了一个隐患,因为在调用点看来,doEeg方法执行是正常的。(当然,你可以在doEeg方法内catch块对这次失败做一处理,并且用返回值等方式上报执行失败。这种方式相对很麻烦)可见,异常的处理地点的选择并非随心所欲。而且用各种方式上报异常的必要性。
不论方法之间是否相互独立,在思路上都要将每一个异常跟踪到程序结束。如果本着这种思路,而不是仅仅捕获或返回,除非你确定这个异常不会产生负面影响而可以让程序继续执行,但这这种情况只适用于小的程序。
RETURN 的隐患。
怎样才能算是跟踪到程序结束呢,
[异常上报]
异常上报,就是当某一点过程抛出异常时用各种方式告知上层,以便得到处理的方式。
1本地处理,然后使用函数返回值来上报。
2 让异常对象上浮到集中处理的地方。上抛异常。
四.深度理解异常。
你为什么要使用异常?
1.异常与一般防错语句的应用比较。
相比而言异常机制的开销要比判断控制语句的大的多。
两者是否可以替换。原则是:可能频繁出现的异常情况应当选用数据校验的方式。而不应当使用异常机制。如被零整除的情况。
异常机制简化了异常情况的处理。如Exception对象就能匹配所有的托管异常对象。而一般换作数据校验则需要考虑许多情况,多重的判断使代码也会很复杂繁琐。另外数据校验方式只能通过返回值来上报,并且它只能就地处理。而异常也可以用异常对象上浮来报告,所以处理地点也很灵活。
2.异常不是错误。程序错误一般分为语法错误和逻辑错误。
应当将运行时异常理解成另外一种可能。所以,当程序中出现无法处理的异常时,可以认为是程序遇到了你没有事先告诉它怎么处理的情况。按照这样的思路,可以做以下设计:如载人飞船在着陆时出现了故障,需要启动飞行员逃生计划。但你不能确保你的方案就一定能执行成功,所以,当A方案失败时,立即启用B方案。
class Safe_Test{
//执行逃生
public void run_safe()
{
try
{
Run_safe_A();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
Run_safe_B();
}
}
//逃生计划A
private void Run_safe_A()
{
Console.WriteLine("-------->>>已经启动计划A... ...");
throw new Exception("逃离计划A -- 失败");//出现了异常
Console.WriteLine("计划A已成功完成。");
}
//备用逃生计划B
private void Run_safe_B()
{
Console.WriteLine("-------->>>已经启动计划B ... ...");
//执行逃生任务
Console.WriteLine("计划B已经成功完成。");
}
}
值得注意的是:该方法的异常处理地点虽然和上例类似,但这次却是正确的。因为不论因为异常情况而启动了那一个计划,调用点关心的是最终是否逃生(A异常影响范围没有扩展到上层)。所以只要有一个计划执行成功,就没有太大必要向上报处理,在本地解决即可。但如果两项都不成功,那就必须上报。