Press "Enter" to skip to content

我们什么时候应该使用异常?

先说个题外话: 在公司做了俩件事, 是我觉得很有意义的, 第一就是成立了一个PHP邮件组, 第二就是成立了一个Hi群. 目前俩者都有超过500 phpers在里面. 我一直认为, 构建一个交流平台, 让同学们能顺畅, 简单的沟通, 是营造积极的技术学习氛围的基础和前提. 让每个人的问题不会成为别人的问题, 则是最直接的利益. (后记: 不少人都问邮件组地址, 实在不好意思, 这个邮件组是公司内部的邮件组, Hi也是公司内部的. 谢谢)
昨天, 有同事在邮件组提了个问题:

PHP应该什么时候使用 Exception ? 它的性能如何?

这个问题也算是一个久经争论的经典问题了. 我谈谈我的个人看法.
异常与之对应的错误码(或者状态码), 到底各自有什么优点, 缺点, 我们应该怎么使用呢?

错误码

首先来说, 异常机制是在错误码机制之后才出现的, 那么根据进化论, 异常自然是避免了错误码机制的一些不足. 这些不足包括.

1. 错误信息不丰富

函数, 只能有一个返回值(当然, Lua可以返回多个, 但其实也相当于在PHP中返回一个数组), 我们见过最多的函数说明就是: 成功时候返回***, 错误的时候返回FALSE, 然而一个函数出错我原因可能有多种, 出错的种类更有多种. 一个简单的FALSE, 并不能把具体的错误信息告诉调用者.
于是, 我们也就见过一些, 这样的函数说明: 如果返回值大于0, 则表示成功的状态码, 如果返回值小于0, 则表示出错的状态码.
然而, 这个要求函数是返回整形(或者数字), 对于一些其他函数, 我们并不能通过0, >0, <0来判别, 并且, 即使通过这样的方式, 我们还需要用返回的错误码和一些预定义宏(或者调用类似strerror())来获取具体的, 可读的错误信息. 于是, 就有一些函数使用全局的错误码, 和错误信息, 来保存具体的错误信息, 这个时候我们就看到这样的函数描述: 成功返回***, 出错的时候返回FALSE, 错误代码保存在全局变量$errno中(至少大多数Linux库函数是这样描述的, 呵呵). Okey, 这样的方式确实可以工作, 但是, 是不是觉得, 很丑陋呢?

2. 加入错误状态码可能需要改变函数签名

假设, 你编写了一个函数, 这个函数很简单, 很简单, 你认为他绝对不会出错, 于是你申明为(用C语言为例, PHP没有返回类型提示):

void dummy() {
}

但是后来你慢慢修改了这个函数, 给了它更多的功能, 此时这个函数可能会失败了. 而你现在根本无法为这个函数, 加入错误返回码了.
也许有人说PHP没有返回值类型限制一说, 但是想想PHP的构造函数, 构造函数是没有返回值的, 当发生错误的时候, 如果你不使用异常, 我想你只能选择die, 或者使用2中的方法来错误继续执行了.
另外, 在一个良好的软件系统中, 返回类型其实也是约定俗成的, 当所有的使用的函数的地方, 都没有检查返回值的时候, 你还是无法为这个函数加入错误返回码.

3. 错误状态码可能会被忽略

当你的一个函数, 出错了, 返回了错误状态码, 而调用方并没有检测这个返回值, 会发生什么情况呢? -_#. 令一方面, 处处检测返回状态码, 会造成代码非常的,,ugly:

<?php
  if (!call1()) {
      die();
  }
  if (call2() != SUCCESS) {
     die();
  }
  if (call3() < 0) {
      $msg = error_get_last();
      die($msg["message"]);
  }

异常机制

那么现在我们来看看异常机制, 如果我们采用异常机制, 上面的代码可以写作:

<?php
try {
   call1();
   call2();
   call3();
} catch (Exception $e) {
   die($e->getMessage());
}

更方便的, 如果你的代码只是中间层, 你的调用方会负责处理错误的话, 你甚至可以简单的写作:

<?php
function myFunc() {
   call1();
   call2();
   call3();
}

而一个异常对象, 可以包含更丰富的错误信息, 比如错误信息, 错误码, 错误的行数, 文件, 甚至出错上下文, 等等, 避免的"1.错误信息不丰富"的不足.
我们也可以为一个返回void类型的函数增加异常, 而不改变他的函数签名, 也就不会有上面说的"2.加入错误状态码可能需要改变函数签名". 对于PHP来说, 如果我们新加入的错误没有被捕捉, 也不用担心, 会明显的出错的. 也就不会发生上面所说的"3. 错误状态码可能会被忽略"的情况.
然而, 也有一些反对使用异常的声音:

1. 性能

正如文章开头提问中的: "它的性能如何?", 异常机制确实要比返回状态码的方式昂贵一些, 对于C++来说, 在异常发生的时候, 还要发生堆栈解退(对于PHP来说, 没有这个逻辑, 具体的大家可以参看我之间写的一篇文章: 深入理解PHP原理之异常机制).
性能和方便, 往往是一个矛盾体, 我只能说, 你需要权衡, 如果你写的是一个小的模块, 并且它的生命期可能很短, 也不需要什么特殊的设计模式, 那我觉得你可以不用异常.
而如果你在为一个庞大的软件做开发, 我想你更应该看重的, 应该是, 它的可扩展性, 可维护性.

2. 太多可能的Uncaught Exception

如果, 你调用了一个可能发生异常的函数, 但是却没有捕获这个异常, okey, Fatal Error了, 所以让我们的代码看起来:

<?php
try {
} catch () {
}
....
try {
} catch () {
}
....
try {
} catch () {
}

然而, 这个是可以经过良好设计避免的, 比如我在设计Yaf的时候, 就提供了全局异常处理, 也就是类似于, 你在最最顶层, 加上了一个try catch, 所有的异常错误逻辑都加到这个里面, 你也可以很方面的把你自己的异常加进去.

结论

经常有人批评我是俩面派, 呵呵, 但是在大家了解了上面的利弊以后, 是否也会和我一样认为: 这个事情没有定论呢? 一切从实际出发. 🙂
说了这么多, 算是抛砖引玉, 欢迎补充, 交流: here or here.

38 Comments

  1. json
    json September 9, 2018

    一次去腾讯面试,在那的朋友说线上环境是不用try{}catch{}的。这样不安全。我也就纳闷了。那try{}catch{}到底该不该用啊?

  2. xpx
    xpx September 9, 2018

    一次去腾讯面试,在那的朋友有线上环境是不用try{}catch{}的。这样不安全。我也就纳闷了。那try{}catch{}到底该不该用啊?

  3. bluebird
    bluebird September 5, 2016

    异常非常实用,我自己也做过框架,也是最顶层加入了try,这样在设计程序功能的时候,只需要按一切正常的逻辑去写代码,任何时候出错,会跳到异常处理的代码块去,很好的解决了代码流程控制。 我曾经写了一个php的confirm函数,在服务端抛出让客户端确认的函数, 还写过 php代码级别的signal,当数据异常的时候,只需要触发一个信号,就可以到数据异常处理的分支。 其实这都是利用了Exception实现的,以前写的函数要写很多返回值,现在只写正常的返回值。

  4. […] 3 个回答 回答: 基本不用try …..catch ,出错的话php会告诉你哪里错了。顶多if else判断下。 回答: 这个还真没太用过,平时写的东西一般用不上参考鸟哥的这篇文章,lz看完应该会有所收获的http://www.laruence.com/2012/02/02/2515.html 回答: 我一般就是包含文件前先检测,如果不存在就抛出异常,美化一下报错的界面哈哈。或者你想统一一下出错后的处理方式会用的着。当然,在一些复杂点的系统里,这还是很有用的,比如可以扩展异常来查出trace 函数的调用步骤等等。不过一般来说这的确是绣花枕头。 本条目发布于 2013 年 4 月 25 日。属于 php 分类,被贴了 php 标签。作者是 admin。 […]

  5. 黄叶
    黄叶 June 28, 2012

    很高兴拜读到鸟哥的精彩文章,
    呵,关于异常的处理,很多时候因为考虑到了不同的需求,所以采用了不同的处理方式,有两种,一种就是Fatal Error类的错误,还有一种就是普通的用户性异常处理,我们现在是这样处理的
    如在网站上出现了一些Fatal Error的错误时,一般是通过特殊的方式捕获到(如通过register_shutdown_function来注册一个CLASS来捕获),接下来就是需要考虑到两方面的问题,第一是,用户在使用网站时遇到的致命性错误时,采用什么提示方式使体验更好,第二就是,技术人员遇到此类错误时,一般都需要通知技术人员去修改好,所以还得把该错误记录下来,使技术人员能动态跟踪到
    第二种错误就是,用户性的操作异常了,如用户输入了一些非法字符,或者说用户做了一些非法操作后的信息提示,需要给予用户相关的提示,以便他能更好的接下来操作,在我们系统上肯定有很多类似的提示,而为了达到一个统一的管理,所以便做了一个统一的异常处理,即把所有的异常处理提示保存一个通用的文件中,而在遇到此类异常时,只是纯粹的throw new,然后再做一个统一的处理。

  6. ramones_kkk
    ramones_kkk May 20, 2012

    异常本质上是程序执行流程的跳转,它最大的好处是可以将处理正常情况的代码和处理错误的分开,使代码可读性提高。
    异常也是处理错误的一种手法,对于大部分可以处理的错误,个人认为无需使用异常,使用错误码或者状态码就可以了。对于不可以恢复和处理的错误,比如说数据库连接失败,内存分配失败等,或者是底层的第三方库抛出了异常。这些情况下才使用异常。
    异常因为要栈回溯,是有一定的开销,但我认为谨慎使用异常的主要原因是,过多的异常处理会带来程序流程的混乱,比如一个大的try块了发生了异常,你比较难定位到底是哪条语句抛出的。

  7. tokimeki
    tokimeki March 14, 2012

    大部分的時候我都是用異常來處理,不過我的工作是寫中間的元件,有時候必須吃掉異常來讓調用的一方繼續執行,可是當這種情況發生時,我並不確知錯誤的訊息調用方需不需要,不知道碰到這種情況有沒有甚麼模式可以參考?

  8. owen
    owen February 29, 2012

    hey,鸟哥.
    请问你的hi群是只对公司内部同事交流么?
    我可以加入么

  9. laruence
    laruence February 8, 2012

    @Rhythm 好注意, 我试着问问Rasmus. 另外这个配色方案好像叫做ir_black..

  10. Rhythm
    Rhythm February 8, 2012

    能否发起,成立一个中国PHP的邮件组。

  11. Rhythm
    Rhythm February 8, 2012

    请问,这个vim的配色方案叫什么名字?

  12. 雪候鸟
    雪候鸟 February 8, 2012

    @pdfdog 这是vim, 不过好像zend ide也有类似的配色方案, 只是我不知道 🙂

  13. dulidong
    dulidong February 8, 2012

    支持下laruence,希望能加入邮件组

  14. pdfdog
    pdfdog February 7, 2012

    博主你的zend主题叫什么啊,好顺眼,麻烦给偶百度下啦~

  15. Peter
    Peter February 7, 2012

    博主你平时用的zend是上面的颜色主题吗??

  16. xiatian
    xiatian February 7, 2012

    我一直有个困惑,通常情况下某个函数或方法只返回true和false,在特定条件下需要抛出一个异常(当然也就没有返回值了),当抛出异常后,原先调用这个函数或者方法的流程自然就没有继续执行了,程序通常会由set_exception_handler定义的处理方式去处理异常了,而此前调用的流程就被阻断,而我又不想对它单独定义try/catch.
    演示伪代码:
    function is_ok(xxx){
    if (not xxx){throw new Exception(‘xxx is not ok’)}
    return true OR false;
    }

    if (is_ok())
    {
    🙂
    }
    else
    {
    🙁
    }
    //抛出异常后上面的就不会执行了,有没有不在这里try/catch的解决办法,我想总是用set_exception_handler去处理所有异常

  17. likemeng
    likemeng February 7, 2012

    希望能加入邮件组!

  18. Iterse's blog
    Iterse's blog February 6, 2012

    支持下,两边都做分析了,哈哈。奢望一下哦,希望能加入您的php邮件组。

  19. phper
    phper February 4, 2012

    求hi群号

  20. blankyao
    blankyao February 2, 2012

    前几天遇到一个问题,类似下面的操作
    stream1.close();
    stream2.close();
    stream3.close();

    这几行代码都有可能抛出异常,但是抛出异常后也得保证后面的代码继续执行,所以就不能在外面搞一个try catch,所以现在是每一行都加一个tray catch,但是感觉比较ugly,有啥好的建议不?

  21. oboodo
    oboodo February 2, 2012

    求hi群号,惠兄!

  22. xiaokaizi
    xiaokaizi February 2, 2012

    class xx{
    public $box[2] = ‘fds’;
    public function echso() {
    // …
    }
    } 这样定义类为什么报语法错误?
    在zend_language_parser.c里,变量名是不允许带[]的,估计在词法分析那一步把$box[2]分割成了一个变量名,fds是值,到语法分析这步就出错了。
    如果是这样原因。问题是为什么要在类的成员变量中这样分割,而普通变量缺不呢?

  23. 岭南六少
    岭南六少 February 2, 2012

    哈哈,现在用hi的人貌似不是很多啊

  24. benson
    benson February 2, 2012

    请问,作为一个production,异常处理的使用是为了在产品发生错误的时候能更好的提示用户,或者通知开发者,是这样理解吗?

  25. lin
    lin February 2, 2012

    能不能加入邮件列表和HI群

  26. darasion
    darasion February 2, 2012

    顶。
    这个问题是我问的。
    多谢,看完这文章明白多了。

  27. treesky
    treesky February 2, 2012

    前排留名啊。学习。HI群有点麻烦,能不能让我们也加入邮件列表啊。

Leave a Reply to phper Cancel reply

Your email address will not be published. Required fields are marked *