谈谈Exception,什么时候抛出?什么时候接住? 一
DOTNET世界中的异常机制
------------------------------
Exception是很基础,可是后期很麻烦的东西。初学者会认为exception仅仅是:
{
}
catch(Exception ex)
{
}
finally
{
}
但是到了后期开发,问题变得复杂。当代码积累到达一定的量(20%使用了.net framework,80%都是自己开发的dll)。那么当我们调用一个method的时候,心里会不清楚是否有存在的危险,结果导致try catch满天飞,却依然抛出了不知哪里的exception。
在Java世界里面,有个checked exception机制;编译器显示的告诉用户当前方法存在的exception,让用户处理,或者再标注throw关键词在method,例如:
{
xxxxx
}
这个看似美妙的设计,在DOTNET里面却被抛弃了,根据dotnet设计者的原话:The Trouble with Checked Exceptions
不引入java的checked exception的原因在于:
1. 版本问题。例如Hello()在version 1里面,抛出ABC三个异常,但是到了version 2里面,会再抛出D。这样就导致了原始调用Hello()的代码失效了。
2. 扩展性问题。假设10个方法每个抛出10个异常,那么当一个高级方法调用这10个方法,就需要处理100个异常了。
这样导致了在代码里面用户简单的throw,没有达到checked exception的实际目的。
虽然到了Enterprise Library里面,出现了 Exception Handling Application Block,可是在我看来,只是把问题“工程化” 而已,并没有从技术角度实际解决exception的问题。例如在通过外部的配置文件,控制当前exception是否应该抛出等。(题外话,这种处理我个人认为不会成熟;因为只要把业务代码分散在多于1个地方,维护难度就指数增加。)
我花了几个小时翻遍了codeproject,里面没有一个人提出成熟的可用的代码(毕竟是dotnet天生不支持。。) ,不过还是有借鉴的文章,例如:Simplifying Exception-Safe Code 提出了一种exception业务回滚的思路。
既然前人没有成果,我只好自己制造了。
Exception的分析
-------------------------------
首先,我们的代码是什么? 或者说,计算机在干什么?
就是给出指定的输入,代码返回符合期望的输出。这个是计算机的基本功能。那么异常就是出现了和上面定义不同的情况。
那么什么时候会出现问题?异常只有在什么时候会被跑出来?
1. 没有给定符合要求的输入。
2. 方法内部由于外部因素(磁盘IO、网络、系统内存等软硬分割区域),产生了超乎预期的操作。
3. 方法内部运算结果没有符合预期的输出结果(例如没有找到)
和
4. 代码存在的bug、缺陷,导致方法无法获取期望的输出。 (这是个非常恶心的东西)
根据这4点,exception就分为了 预期的异常expectedException 和意外的异常unexpectedException.
预期的异常,仅仅用于程序流程控制,而不需要维护、即不需要最终展示给维护人员。因此处理过程中仅仅需要记录、拦截、抛出。
意外的异常,一定会被抛出来,需要详细记录。这个是需要仔细研究,修复bug的。
预期的异常 ExpectedException
--------------------------------------
预期的异常包括了:
1. 没有给定符合要求的输入。(VerificationFailedException )
2. 方法内部由于外部因素(磁盘IO、网络、系统内存等软硬分割区域),产生了超乎预期的操作。 (UnexpectedExternalException )
3. 方法内部运算结果没有符合预期的输出结果(例如没有找到) (UnhandledResultException)
由于exception是可控制的,我们也知道什么地方会抛出异常,抛出了如何处理。 因此设计exception是否应该抛出的时候,就得到:
1. 当前方法是否允许抛出异常。
2. 当前方法的返回值能否代表异常。
有些方法,是不允许抛出异常的,特别是void的方法,我们并没有期望他们能够一定成功,所以这种方法不需要设计exception,直接try catch就结束了。
有些方法,返回值能代表异常,例如:获取当前温度,如果返回-1,表示获取失败。等等。这种情况下,这种方法也不需要抛出异常,用try catch全部包住。
那么,剩下的方法,就是会抛出异常的了。
这里就来到了一个有趣的逻辑点:由于系统都是由无数的方法嵌套调用而成的,那么最坏情况下是最顶端的方法不抛出异常,也就符合了上面提到的2点中的一点。例如我们的
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form2());
}
因此,到目前为止我的分析都是对的,而且在 预期的异常 这部分,已经在数学上实现了一个有限的集合。 所以处理就变得简单了:我们只需要处理两方面:
1. 封锁异常,不再抛出。这种情况,所有的exception都会被拦截并且日志记录下来。没有什么难度。
2. 抛出异常并传递。
由于上文分析了,预期的异常只有三种,所以一个方法如果抛异常,也最多只有3个,这样在throw的传递过程中,我们就非常容易实现了filter功能。从而 消除了 开篇提到了checked exception的2个问题。
当当前方法会抛出异常的时候,我们要控制需要抛出哪些异常。如果异常来自自身就很简单,问题就是异常来自调用的子方法传递出来的。这个时候就需要策略。
2.1. verificationFailedException。任何会抛出异常的方法都会潜在拥有这个异常。这个方法是否会抛出输入验证异常,和子方法无关。即子方法的输入验证在这个方法一定都被默认的try catch了。结论是:异常不会传递。但是会转变为当前的输入验证异常。
例如
MethodA()
{
MethodB();
}
如果methodB存在输入错误。那么methodA默认已经处理了。除非methodA也存在输入错误,那么这个校验就传递到了外界。而这个exception是从methodA抛出来的,和methodB无关。
2.2. UnexpectedExternalException 是否传递?一个方法
MethodA()
{
Call1();//net io
Call2();//file io
Call3();//file io
}
到目前为止,这种级别的异常,都会被传递出去,而不被记录。似乎没有反例。所以结论是:继续抛出,不被记录,直到不抛出的地方。
2.3. UnhandledResultException 本质来说,这种异常和输入错误异常是一致的。因为我调用的方法返回值,等价于外界传入一个参数。所以reduce to 2.1 的处理。
所以,一个方法可能抛出的异常中:
1. 来源于自己的验证。
3. 来源于自己的,如果是子类的,会规划了1的处理机制,即如果输入参数不符合会抛异常,就抛;否则就不抛,自己处理。
2. 来源于子类、自己,处理机制就是继续抛出,直到遇到不抛出的类。
意外的异常 UnexpectedException
--------------------------------------
一定来源于bug。也一定被抛出。处理机制在不抛出的类中。也以往的一致。一般这种exception发源地一定是外部的dll,不是自己的。所以也好控制。不涉及自己的代码维护。