异常本身的语法并不值得讨论,异常的使用场景才是主要的,这里我对比php和java,来看看php里的异常到底是怎么回事,异常到底应该怎么用。
看到了PPC论坛上的这篇讨论,觉得很有价值,我重新整理了下我的观点,做个总结。
首先,需要说的是,这里的异常是指php的异常。因为php的异常和其它语言相比有着很大的不同。
php里的异常,是程序运行中的不符合预期的情况,即一种在程序执行流程里面允许发生,只是和正常流程不同的状况。它是一种不正常的情况,就是按照我们的正常逻辑本不该出错,但仍然会出现的错误,属于逻辑和业务流程的错误,而不是语法上的错误。
php里的错误则是一种非法的,语法或者环境问题导致的让编译器无法通过检查,甚至无法运行的情况。
php的异常处理所做的是对你程序运行时出现的某种情况进行处理,并不是错误,异常是程序运行得到的结果不是你想要的。对于程序而言,异常是不可控的,我们无法控制运行时在哪个环节会出错,但是我们可以大致预期到哪些环节会出错,并进行针对性的补救。
异常(exception)和错误(error)的概念以及区分在各种语言里是不一样的。在java和php里,对错误和异常的界定也是不同的。在php里,它遇到任何的自身错误都会触发一个错误,而不是抛异常(对于一些情况,会同时抛出异常和错误)。php一旦遇到非正常的代码,通常都会触发错误,而不是抛出异常。在这个意义上,如果你想使用异常来处理不可预料的问题,是办不到的。比如说,你想在文件不存在,数据库连接打不开的时候触发异常,是不可行的。这在php里是一种错误,php把它作为错误抛出,而无法作为异常自动捕获。而java则不同,java把很多行为看成是异常并且可捕获。
我们来个最直观最简单的例子吧。就以经典的除零问题为例:
代码如下 | 复制代码 |
//exception.php $a=null; try{ $a=5/0; echo $a,PHP_EOL; }catch(exception $e){ $e->getMessage(); $a=-1; } echo $a; |
运行结果:
下面是java代码:ExceptionTry.java
代码如下 | 复制代码 |
//ExceptionTry.java public class ExcepetionTry { public static void tp() throws ArithmeticException{ int a; a=5/0; System.out.println("运算结果:"+a); } public static void main(String[] args) { int a; try { a=5/0; System.out.println("运算结果:"+a); } catch (ArithmeticException e) { e.printStackTrace(); }finally{ a = -1; System.out.println("运算结果:"+a); } try { ExcepetionTry.tp(); } catch (Exception e) { System.out.println("异常被捕获"); } } } |
运行结果:
如果我们把tp方法中的第二条语句改为如下:
a=5/1;
那么结果将是如下:
由以上运行结果可以看到,对于除0这种“异常”代码,php认为这是一个错误,会直接触发错误(waring也是错误,只是错误等级不一样而已),而不会自动抛异常使进入异常流程,故最终$a的值并不是预想中的-1,也就是说,并没有进入异常分支,也没有处理异常。php只有你主动throw后,才能捕获异常(一般情况是这样的,也有一些异常php可以自动捕获)。
在下面三种场景下会用到异常处理机制:
(1)对程序的悲观预测
如果一个程序员对自己的代码带有“悲观情绪”,这里并不是指该程序员代码质量不高。他认为自己的代码无法一一处理各种可预见的不可预见的情况,那该程序员就会进行异常处理。假设一个场景,程序员悲观地认为自己的这段代码在高并发条件下可能产生死锁,那么他就会悲观地抛出异常,然后在死锁时进行捕获,对异常进行细致的处理。
(2)程序的需要和对业务的关注
如果程序员希望业务代码中不会充斥大堆的打印,调试等处理,通常他们会使用异常机制;或者业务上需要定义一些自己的异常,这个时候就需要自定义一个异常, 来对现实世界中各种各样的业务进行补充。比如上班迟到,这种情况,我就认为是一个异常,要收集起来,到月底集中处理,扣你工资;如果程序员希望有预见性地处理可能发生的会影响正常业务的代码,那么它需要异常。在这里,强调了异常是业务处理中必不可少的环节,不能对异常视而不见。异常机制认为,数据一致很重要,在数据一致性可能被破坏时,就需要异常机制来进行预先补救。
举个例子,比如有个上传文件的业务需求,要把上传的文件保存在一个目录里,并在数据库里插入这个文件的记录,那么这两步就是互相关联密不可分的一个集成的业务,缺一不可。文件保存失败,而插入记录成功就会导致无法下载文件;而文件保存成功数据库写入失败,则会导致没有记录的文件成为死文件,永远得不到下载。
那么我们假设文件保存成功后没有提示,但是保存失败会自动抛出异常,访问数据库也一样,插入成功没有提示,失败则自动抛出异常,我们就可以把这两个有可能抛出异常的代码段包在一个try语句里,然后在catch捕捉错误,在catch代码段里删除没有被记录到数据库的文件或者删除没有文件的记录,以保证业务数据的一致性。 因此,从业务这个角度讲,异常偏重于保护业务数据一致性,并且强调了对异常业务的处理。
如果我们的代码中,只是象征性的try-catch,最后打印一个报错,over。这样的异常,不如不用,没有体现了异常的思想。所以,合理的代码应该如下:
代码如下 | 复制代码 |
try{ //可能出错的代码段 if(文件上传不成功) throw(上传异常); if(插入数据库不成功) throw(数据库操作异常);}catch(异常){ 必须的补救措施,如删除文件,删除数据库插入记录,这个处理很细致 } //.... ?> |
也可以如下:
代码如下 | 复制代码 |
上传{ if(文件上传不成功) throw(上传异常); if(插入数据库不成功) throw(数据库操作异常); } //其他代码...try{ 上传; 其他; }catch(上传异常){ 必须的补救措施,如删除文件,删除数据库插入记录 }catch(其它异常){ 记录log } ?> |
上面的两种捕获异常的方式,前一种是在异常发生时,立刻捕获;后一种是分散抛异常,集中捕获。那到底应该是哪一种呢?
如果我们的业务很重要,那么异常越早处理越好,以保证程序在意外情况下能保持业务处理的一致性。比如一个操作有多个前提步骤,突然最后一个步骤异常了,那么其他前提操作都要消除掉才行,保证数据一致性。并且在这种核心业务下,有大量的代码来做善后工作,进行数据补救,这是一种比较悲观的异常。
如果我们的异常不是那么重要,并且在单一入口,MVC风格的应用中,为了保持代码流程的统一,则常常采用后一种异常处理方式,这种异常处理更多强调了业务流程的走向,对善后工作并不是很关心。这是一种乐观的异常。
(3)语言级别的健壮性要求
在这点上,php是缺失的。以java为例,java是一种面向企业级开发的语言,强调健壮性。java中支持多线程,java认为,多线程被中断这种情况是彻彻底底的无法预料和避免的。所以 java规定,凡是用了多线程,就必须正视这种情况。你要么抛出,不管它,要么捕获,进行处理。总之,你必须面对 InterruptedException这个异常,不准回避。也就是异常发生后应对重要数据业务进行补救,当然你可以不做,但是我会告诉你,这是你应该做的。 这类异常是强制的。更多的异常是非强制的,由程序员决定的。java对异常的这种分类和约束,保证了java程序的健壮性和可信赖度。
那么异常的意义何在?
异常就是无法控制的运行时错误,会导致出错时中断正常逻辑运行,该异常代码后面的逻辑都不能继续运行。那么try/catch的好处就是可以把异常造成的逻辑中断破坏降到最小范围内,并且经过补救处理措施后不影响业务逻辑的完整性,乱抛异常和只抛不捕获,或捕获而不补救,会导致数据混乱。 这就是异常处理的一个重要作用,就是在我们精确控制运行时流程的时候,在程序中断的时候,有预见的用try缩小可能出错的影响范围,再及时捕获异常的发生并作出相应的补救,以使逻辑流程仍然能回到正常轨道上来。
怎样看php的异常?
我们已经看到了php中的异常机制是很鸡肋的,绝大多数情况下无法自动抛异常,必须用if-else来先进行判断,再手工抛出异常。这种处理方式看起来,比较像是多此一举。手动抛异常的意义就不是很大了,因为你手动抛异常也就意味着你在代码里已经充分预期到错误的出现了,也就算不得真正的“异常”了,而是意料之中的了。还是陷入了纷繁复杂的业务逻辑判断和处理中。java和C++语言做的比较好的就是定义了一堆内置的常见的异常,不需要程序员判断各种异常情况后手工抛出,编译器会代我们进行判断业务是否发生错误,自动抛出异常。作为程序员,则只需要关心异常的捕获和随后补救,而不是像php中关注到底会发生哪些异常啊,用if-else来逐一判断,逐一抛异常。
php的异常机制很不完美,很多情况下和if-else相比没有明显的优势,这也是php的异常没有普及的原因。当然了,使用了异常也能一定程度上降低耦合性。
那怎么来完善php原先的异常处理机制呢?这时,就要借助php的错误处理了。PHP提供了一个set_error_handler函数,可以自定义错误处理函数,能够把非致命类型的错误处理都转向到自己定义的函数里进行分析和处理。但是因为出错的地方可能很多,集中处理的话要区分的情况很复杂,所以我们只用这个特性做个跳板。在自定义函数里我们手动抛一个异常出来,杀个回马枪,让try/catch可以捕获并处理这个运行时错误所带来的中断,从而实现扩大try/catch影响范围的目的.