错误、异常与自定义异常

程序员对于异常(Exception)这个词应该都不陌生,尤为如今Exception基本上是OOP编程语言的标配。于我而言,这个词既熟悉又陌生,熟悉是由于听过了不少遍、彷佛也有大量使用;陌生是由于不多真正思考过到底什么是异常,以及如何使用异常。本文记录我对如何使用异常、自定义异常的一些见解,不必定正确,还请多多指教。
本文地址:http://www.javashuo.com/article/p-rtdqeewa-cz.htmlhtml

什么是异常

异常是错误处理的一种手段:java

exception handling is an error-handling mechanismpython

上述定义中的error是广义的error,任何代码逻辑、操做系统、计算机硬件上的非预期的行为都是error。并非Java语言中与Exception对立的Error(Java中,Error和Exception是有区别的,简而言之,Error理论上不该该被捕获处理,参见Differences between Exception and Error),也不是golang中与panic对立的error。linux

在编程语言中,对于error的分类,大体能够分为Syntax errors、Semantic errors、Logical errors,若是从error被发现的时机来看,又能够分为Compile time errors、Runtime errors。git

结合实际的编程语言,以及wiki上的描述:程序员

Exception handling is the process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional conditions requiring special processing – often disrupting the normal flow of program execution.github

能够看出,通常来讲,Exception对应的是Runtime error,好比下面的代码golang

FileReader f = new FileReader("exception.txt"); //Runtime Error

若是文件不存在,就会抛出异常,但只有当程序运行到这一行代码的时候才知道文件是否存在。编程

须要注意的是,异常并非错误处理的惟一手段,另外一种广为使用的方式是error codeerror code是一种更为古老的错误处理手段,下一章节将会就error code与exception的优劣介绍。设计模式

何时使用异常

下面用两个例子来阐释何时使用异常。

初探异常

第一个例子来自StackExchange When and how should I use exceptions? .
题主须要经过爬取一些网页,如http://www.abevigoda.com/来判断Abe Vigoda(教父扮演者)是否还在世。代码以下:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

def parse_abe_status(s):
    '''Param s: a string of the form "Abe Vigoda is something" and returns the "something" part'''
    return s[13:]

简而言之,就是下载网页内容,提取全部包含"Abe Vigoda"的句子,解析第一个句子来判断"Abe Vigoda"是否尚在人世。

上述的代码可能会出现几个问题:

  • download_page因为各类缘由失败,默认抛出IOError
  • 因为url错误,或者网页内容修改,hits可能为空
  • 若是hits[0]再也不是"Abe Vigoda is something" 这种格式,那么parse_abe_status返回的既不是alive,也不是dead,与预期(代码注释)不相符

首先,对于第一个问题,download_page可能抛出IOError,根据函数签名,函数的调用者能够预期该函数是须要读取网页,那么抛出IOError是能够接受的。

而对于第二个问题 -- hits可能为空,题主有两个解决方案。

使用error code

在这里,就是return None

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

显然,这里是经过error code(None)来告诉调用者错误的发生,上一章节也提到 error code是除了Exception handling以外的另外一种普遍使用的error handling 手段。

那么error code相比Exception有哪些优缺点呢?
首先是优势:

  • 没有引入新的概念,仅仅是普通的函数调用
  • 易于理解,不会打乱当前的执行流

相比Exception,其缺点包括:

  • 代码时刻都须要检查返回值,而调用者很容易遗漏某些检查,这就可能隐藏、推迟更严重问题的暴露

  • 缺少错误发生的上下文信息
  • 有的时候一个函数根本没有返回值(好比构造函数),这个时候就得依赖全局的error flag(errno)

好比Linux环境下,linux open返回-1来表示发生了错误,但具体是什么缘由,就得额外去查看errno

回到上述代码,从函数实现的功能来讲,check_abe_is_alive应该是比get_abe_status更恰当、更有表达力的名字。对于这个函数的调用者,预期返回值应该是一个bool值,很难理解为何要返回一个None。并且Python做为动态类型语言放大了这个问题,调用极可能对返回值进行conditional execution,如if check_abe_is_alive(url):, 在这里None也被当成是False来使用,出现严重逻辑错误。

返回None也体现了error code的缺点:延迟问题的暴露,且丢失了错误发生的上下文。好比一个函数应该返回一个Object,结果返回了一个None,那么在使用这个返回值的某个属性的时候才会出trace,但使用这个返回值的地方可能与这个返回值建立的地方已经隔了十万八千里。没有让真正的、原始的错误在发生的时候就马上暴露,bug查起来也不方便。

抛出异常

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

注意上面的代码同时也包含了第三个问题的解决方案,即确保statusalive或者dead两者之一。不过咱们重点关注对hits为空的处理。有两点值得注意:

  1. 抛出的是自定义异常NotFoundError,而不是IndexError。这是一个明智的选择,由于hits为空是一个实现细节,调用者很难想象为啥要抛出IndexError。关于自定义异常,后面还有专门的章节讨论。
  2. 经过尝试捕获IndexError来判断hits为空,这个是不太推荐的作法,由于这里明显能够经过if not hits来判断hits是否为空

关于用条件判断(if) 仍是 try-catch, 在Best practices for exceptions中是这样描述的

Use exception handling if the event doesn't occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). When you use exception handling, less code is executed in normal conditions.

Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

if 仍是 try-catch,其实暗示了关于异常自己一个有争议的点:那就是exception是否应该充当流程控制的手段,wiki上总结说不一样的语言有不一样的偏好。不过,我的认为,若是能用if,就不要使用try-catch,exception仅仅使用在真正的异常状况。

再探异常

第二个例子来自stackoverflow When to throw an exception ,题主的习惯是针对任何非预期的状况都定义、抛出异常,如UserNameNotValidException, PasswordNotCorrectException, 但团队成员不建议这样作,所以题主发帖寻求关于异常使用的建议。

我想这是一个咱们均可能遇到的问题,捕获并处理异常相对简单,但何时咱们应该抛出异常呢,该抛出标准异常仍是自定义异常呢?咱们先看看StackOverflow上的回答

高票答案1:

My personal guideline is: an exception is thrown when a fundamental assumption of the current code block is found to be false.
答主举了一个Java代码的例子:判断一个类是否是List<>的子类,那么理论上不该该抛出异常,而是返回Bool值。可是这个函数是有假设的,那就是输入应该是一个类,若是输入是null,那么就违背了假设,就应该抛出异常。

高票答案2:

Because they're things that will happen normally. Exceptions are not control flow mechanisms. Users often get passwords wrong, it's not an exceptional case. Exceptions should be a truly rare thing, UserHasDiedAtKeyboard type situations.
答主直接回答题主的问题,强调异常应该是在极少数(预期以外)状况下发生的错误才应该使用,异常不该该是流程控制的手段

高票答案3:

My little guidelines are heavily influenced by the great book "Code complete":

  • Use exceptions to notify about things that should not be ignored.
  • Don't use exceptions if the error can be handled locally
  • Make sure the exceptions are at the same level of abstraction as the rest of your routine.
  • Exceptions should be reserved for what's truly exceptional.
    答主参考《代码大全》认为仅仅在出现了当前层次的代码没法处理、也不能忽略的错误时,就应该抛出异常。并且异常应该仅仅用于真正的异常状况。

高票答案4:

One rule of thumb is to use exceptions in the case of something you couldn't normally predict. Examples are database connectivity, missing file on disk, etc.
异常应该仅仅因为意料以外、不可控的状况,如数据链接,磁盘文件读取失败的状况

高票答案5:

Herb Sutter in his book with Andrei Alexandrescu, C++ Coding Standards: throw an exception if, and only if

  • a precondition is not met (which typically makes one of the following impossible) or
  • the alternative would fail to meet a post-condition or
  • the alternative would fail to maintain an invariant.

从上述回答能够看出,若是违背了程序(routine)的基本假设(assumption、prediction、setup、pre-condition)h或者约束(post-condition、invariant),且当前层次的代码没法恰当处理的时候就应该抛出异常。

现代软件的开发模式,好比分层、module、component、third party library使得有更多的地方须要使用异常,由于被调用者没有足够的信息来判断应该如何处理异常状况。好比一个网络连接库,若是链接不上目标地址,其应对策略取决于库的使用者,是重试仍是换一个url。对于库函数,抛出异常就是最好的选择。

自定义异常

在上一章节中咱们已经看到了自定义异常(NotFoundError)的例子.

程序员应该首先熟悉编程语言提供的标准异常类,须要的时候尽可能选择最合适的标准异常类。若是标准异常类不能恰如其分的表达异常的缘由时,就应该考虑自定义异常类,尤为是对于独立开发、使用的第三方库。

自定义异常有如下优势:

  • 类名暗示错误,可读性强, 这也是标准库、第三方库也有不少异常类的缘由
  • 方便业务逻辑捕获处理某些特定的异常
  • 可方便添加额外信息

    For example, the FileNotFoundException provides the FileName property.

Why user defined exception classes are preferred/important in java?中也有相似的描述

To add more specific Exception types so you don't need to rely on parsing the exception message which could change over time.
You can handle different Exceptions differently with different catch blocks.

通常来讲,应该建立框架对应的特定异常类,框架里面全部的异常类都应该从这个类继承,好比pymongo

class PyMongoError(Exception):
    """Base class for all PyMongo exceptions."""


class ProtocolError(PyMongoError):
    """Raised for failures related to the wire protocol."""


class ConnectionFailure(PyMongoError):
    """Raised when a connection to the database cannot be made or is lost."""

异常使用建议

在知道何时使用异常以后,接下来讨论如何使用好异常。

下面提到的实践建议,力求与语言无关,内容参考了9 Best Practices to Handle Exceptions in JavaBest practices for exceptions

Exception应该包含两个阶段,这两个阶段都值得咱们注意:

  • Exception initialization:经过raise(throw)抛出一个异常对象,该对象包含了错误发生的上下文环境
  • Exception handling,经过try - catch(expect) 来处理异常,一般也会经过finally(ensure)来处理一下不管异常是否发生都会执行的逻辑,以达到异常安全,好比资源的释放。

try-catch-finally
try-catch-finally代码块就像事务,不管是否有异常发生,finally语句都将程序维护在一种可持续,可预期的状态,好比上面提到的资源释放。不过为了防止忘掉finally的调用,通常来讲编程语言也会提供更友好的机制来达到这个目的。好比C++的RAII,python的with statement,Java的try-with-resource

若是能够,尽可能避免使用异常
前面提到,exception应该用在真正的异常状况,并且exception也会带来流程的跳转。所以,若是能够,应该尽可能避免使用异常。``Specail case Pattern```就是这样的一种设计模式,即建立一个类或者配置一个对象,用来处理特殊状况,避免抛出异常或者检查返回值,尤为适合用来避免return null。

自定义异常, 应该有简明扼要的文档
前面也提到,对于第三方库,最好先有一个于库的意图相匹配的异常基类,而后写好文档。

exception raise
对于抛出异常的函数,须要写好文档,说清楚在什么样的状况下会抛出什么样的异常;并且要在异常类体系中选择恰到好处的异常类,Prefer Specific Exceptions

clean code vs exception
《clean code》建议第三方库的使用者对第三方库可能抛出的异常进行封装:一是由于对这些异常的处理手段通常是相同的;二是可让业务逻辑于第三方库解耦合。

In fact, wrapping third-party APIs is a best practice. When you wrap a third-party API, you minimize your dependencies upon it

exception handling
捕获异常的时候,要从最具体的异常类开始捕获,最后才是最宽泛的异常类,好比python的Exception

In catch blocks, always order exceptions from the most derived to the least derived

程序员应该认真对待异常,在项目中看到过诸多这样的python代码:

try:
    # sth 
except Exception:
    pass

第一个问题是直接捕获了最宽泛的类Exception;其次并无对异常作任何处理,掩耳盗铃,固然,实际中也多是打印了一条谁也不会在意的log。

若是咱们调用了一个接口,而这个接口可能抛出异常,那么应该用当前已有的知识去尽力处理这个异常,若是当前层次实在没法处理,那么也应该有某种机制来通知上一层的调用者。checked exception确定是比函数文档更安全、合适的方法,不过诸多编程语言都没有checked exception机制,并且《clean code》也不推荐使用checked exception,由于其违背了开放关闭原则,可是也没有提出更好的办法。

Wrap the Exception Without Consuming It
有的时候抛出自定义的异常可能会比标准异常更有表达力,好比读取配置文件的时候 ConfigError(can not find config file)IoError更合适,又好比前面例子中的NotFoundError

不过,重要的是要保留原始的trace stack,而不是让re-raise的stack。好比如下Java代码:

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something may raise NumberFormatException
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e);
    }
}

python2中是下面的写法

def bar():
    try:
        foo()
    except ZeroDivisionError as e:
        # we wrap it to our self-defined exception
        import sys
        raise MyCustomException, MyCustomException(e), sys.exc_info()[2]

references

相关文章
相关标签/搜索